blog's Issues
【转载】Python 中的进程、线程、协程、同步、异步、回调
上下文切换技术
简述
在进一步之前,让我们先回顾一下各种上下文切换技术。
不过首先说明一点术语。当我们说“上下文”的时候,指的是程序在执行中的一个状态。通常我们会用调用栈来表示这个状态——栈记载了每个调用层级执行到哪里,还有执行时的环境情况等所有有关的信息。
当我们说“上下文切换”的时候,表达的是一种从一个上下文切换到另一个上下文执行的技术。而“调度”指的是决定哪个上下文可以获得接下去的CPU时间的方法
进程
进程是一种古老而典型的上下文系统,每个进程有独立的地址空间,资源句柄,他们互相之间不发生干扰。
每个进程在内核中会有一个数据结构进行描述,我们称其为进程描述符。这些描述符包含了系统管理进程所需的信息,并且放在一个叫做任务队列的队列里面。
很显然,当新建进程时,我们需要分配新的进程描述符,并且分配新的地址空间(和父地址空间的映射保持一致,但是两者同时进入COW状态)。这些过程需要一定的开销。
进程状态
忽略去linux内核复杂的状态转移表,我们实际上可以把进程状态归结为三个最主要的状态:就绪态,运行态,睡眠态。这就是任何一本系统书上都有的三态转换图1。
就绪和执行可以互相转换,基本这就是调度的过程。而当执行态程序需要等待某些条件(最典型就是IO)时,就会陷入睡眠态。而条件达成后,一般会自动进入就绪。
阻塞
当进程需要在某个文件句柄上做IO,这个fd2又没有数据给他的时候,就会发生阻塞。具体来说,就是记录XX进程阻塞在了XX fd上,然后将进程标记为睡眠态,并调度出去。当fd上有数据时(例如对端发送的数据到达),就会唤醒阻塞在fd上的进程。进程会随后进入就绪队列,等待合适的时间被调度。
阻塞后的唤醒也是一个很有意思的话题。当多个上下文阻塞在一个fd上(虽然不多见,但是后面可以看到一个例子),而且fd就绪时,应该唤醒多少个上下文呢?传统上应当唤醒所有上下文,因为如果仅唤醒一个,而这个上下文又不能消费所有数据时,就会使得其他上下文处于无谓的死锁中。
但是有个著名的例子——accept,也是使用读就绪来表示收到的。如果试图用多个线程来accept会发生什么?当有新连接时,所有上下文都会就绪,但是只有第一个可以实际获得fd,其他的被调度后又立刻阻塞。这就是惊群问题thundering herd problem。
现代linux内核已经解决了这个问题,方法惊人的简单——accept方法加锁。
(inet_connection_sock.c:inet_csk_wait_for_connect)
线程
线程是一种轻量进程,实际上在linux内核中,两者几乎没有差别,除了一点——线程并不产生新的地址空间和资源描述符表,而是复用父进程的。
但是无论如何,线程的调度和进程一样,必须陷入内核态。
传统网络服务模型
进程模型
为每个客户分配一个进程。优点是业务隔离,在一个进程中出现的错误不至于影响整个系统,甚至其他进程。Oracle传统上就是进程模型。缺点是进程的分配和释放有非常高的成本。因此Oracle需要连接池来保持连接减少新建和释放,同时尽量复用连接而不是随意的新建连接。
线程模型
为每客户分配一个线程。优点是更轻量,建立和释放速度更快,而且多个上下文间的通讯速度非常快。缺点是一个线程出现问题容易将整个系统搞崩溃。
一个例子
py_http_fork_thread.py
在这个例子中,线程模式和进程模式可以轻易的互换。
如何工作的:
- 父进程监听服务端口
- 在有新连接建立的时候,父进程执行fork,产生一个子进程副本
- 如果子进程需要的话,可以exec(例如CGI)
- 父进程执行(理论上应当先执行子进程,因为exec执行的快可以避免COW)到accept后,发生阻塞
- 上下文调度,内核调度器选择下一个上下文,如无意外,应当就是刚刚派生的子进程
- 子进程进程进入读取处理状态,阻塞在read调用上,所有上下文均进入睡眠态
- 随着SYN或者数据报文到来,CPU会唤醒对应fd上阻塞的上下文(wait_queue),切换到就绪态,并加入调度队列
- 上下文继续执行到下一个阻塞调用,或者因为时间片耗尽被挂起
评价
- 同步模型,编写自然,每个上下文可以当作其他上下文不存在一样的操作,每次读取数据可以当作必然能读取到。
- 进程模型自然的隔离了连接。即使程序复杂且易崩溃,也只影响一个连接而不是在整个系统。
- 生成和释放开销很大(效率测试的进程fork和线程模式开销测试),需要考虑复用。
- 进程模式的多客户通讯比较麻烦,尤其在共享大量数据的时候。
性能
thread模式,虚拟机:
1: 909.27 2: 3778.38 3: 4815.37 4: 5000.04 10: 4998.16 50: 4881.93 100: 4603.24 200: 3445.12 500: 1778.26 (出现错误)
fork模式,虚拟机:
1: 384.14 2: 435.67 3: 435.17 4: 437.54 10: 383.11 50: 364.03 100: 320.51 (出现错误)
thread模式,物理机:
1: 6942.78 2: 6891.23 3: 6584.38 4: 6517.23 10: 6178.50 50: 4926.91 100: 2377.77
注意在Python
中,虽然有GIL
,但是一个线程陷入到网络IO
的时候,GIL
是解锁的。因此从调用开始到调用结束,减去CPU切换到其他上下文的时间,是可以多线程的。现象是,在此种状况下可以观测到短暂的Python CPU用量超过100%。
如果执行多个上下文,可以充分利用这段时间。所观测到的结果就是,只能单核的Python
,在小范围内,其随着并发数上升,性能居然会跟着上升。如果将这个过程转移到一台物理机上执行,那么基本不能得出这样的结论。这主要是因为虚拟机上内核陷入的开销更高。
C10K 问题
描述
当同时连接数在10K左右时,传统模型就不再适用。实际上在效率测试报告的线程切换开销一节可以看到,超过1K后性能就差的一塌糊涂了。
进程模型的问题:
在C10K的时候,启动和关闭这么多进程是不可接受的开销。事实上单纯的进程fork模型在C1K时就应当抛弃了。
Apache的prefork模型,是使用预先分配(pre)的进程池。这些进程是被复用的。但即便是复用,本文所描述的很多问题仍不可避免。
线程模式的问题
从任何测试都可以表明,线程模式比进程模式更耐久一些,性能更好。但是在面对C10K还是力不从心的。问题是,线程模式的问题出在哪里呢?
内存?
有些人可能认为线程模型的失败首先在于内存。如果你这么认为,一定是因为你查阅了非常老的资料,并且没仔细思考过。
你可能看到资料说,一个线程栈会消耗8M内存(linux默认值,ulimit可以看到),512个线程栈就会消耗4G内存,而10K个线程就是80G。所以首先要考虑调整栈深度,并考虑爆栈问题。
听起来很有道理,问题是——linux的栈是通过缺页来分配内存的(How does stack allocation work in Linux?),不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。但是内存一旦被分配,就很难回收(除非线程结束),这是线程模式的缺陷。
这个问题提出的前提是,32位下地址空间有限。虽然10K个线程不一定会耗尽内存,但是512个线程一定会耗尽地址空间。然而这个问题对于目前已经成为主流的64位系统来说根本不存在。
内核陷入开销?
所谓内核陷入开销,就是指CPU从非特权转向特权,并且做输入检查的一些开销。这些开销在不同的系统上差异很大。
线程模型主要通过陷入切换上下文,因此陷入开销大听起来有点道理。实际上,这也是不成立的。线程在什么时候发生陷入切换?正常情况下,应当是IO阻塞的时候。同样的IO量,难道其他模型就不需要陷入了么?只是非阻塞模型有很大可能直接返回,并不发生上下文切换而已。
效率测试报告的基础调用开销一节,证实了当代操作系统上内核陷入开销是非常惊人的小的(10个时钟周期这个量级)。
线程模型的问题在于切换成本高
熟悉linux内核的应该知道,近代linux调度器经过几个阶段的发展。
- linux2.4的调度器
O(1)
调度器CFS
实际上直到O(1)
,调度器的调度复杂度才和队列长度无关。在此之前,过多的线程会使得开销随着线程数增长(不保证线性)。
O(1)
调度器看起来似乎是完全不随着线程的影响。但是这个调度器有显著的缺点——难于理解和维护,并且在一些情况下会导致交互式程序响应缓慢。
CFS
使用红黑树管理就绪队列。每次调度,上下文状态转换,都会查询或者变更红黑树。红黑树的开销大约是O(logm)
,其中m大约为活跃上下文数(准确的说是同优先级上下文数),大约和活跃的客户数相当。
因此,每当线程试图读写网络,并遇到阻塞时,都会发生O(logm)
级别的开销。而且每次收到报文,唤醒阻塞在fd
上的上下文时,同样要付出O(logm)
级别的开销。
分析
O(logm)
的开销看似并不大,但是却是一个无法接受的开销。因为IO阻塞
是一个经常发生的事情。每次IO阻塞
,都会发生开销。而且决定活跃线程数的是用户,这不是我们可控制的。更糟糕的是,当性能下降,响应速度下降时。同样的用户数下,活跃上下文会上升(因为响应变慢了)。这会进一步拉低性能。
问题的关键在于,http服务
并不需要对每个用户完全公平,偶尔某个用户的响应时间大大的延长了是可以接受的。在这种情况下,使用红黑树去组织待处理fd列表(其实是上下文列表),并且反复计算和调度,是无谓的开销。
多路复用
简述
要突破C10K
问题,必须减少系统内活跃上下文数(其实未必,例如换一个调度器,例如使用RT的SCHED_RR),因此就要求一个上下文同时处理多个链接。而要做到这点,就必须在每次系统调用读取或写入数据时立刻返回。否则上下文持续阻塞在调用上,如何能够复用?这要求fd
处于非阻塞状态,或者数据就绪。
上文所说的所有IO操作,其实都特指了他的阻塞版本。所谓阻塞,就是上下文在IO调用上等待直到有合适的数据为止。这种模式给人一种“只要读取数据就必定能读到”的感觉。而非阻塞调用,就是上下文立刻返回。如果有数据,带回数据。如果没有数据,带回错误(EAGAIN)。因此,“虽然发生错误,但是不代表出错”。
但是即使有了非阻塞模式,依然绕不过就绪通知问题。如果没有合适的就绪通知技术,我们只能在多个fd
中盲目的重试,直到碰巧读到一个就绪的fd
为止。这个效率之差可想而知。
在就绪通知技术上,有两种大的模式——就绪事件通知
和异步IO
。其差别简要来说有两点。就绪通知维护一个状态,由用户读取。而异步IO由系统调用用户的回调函数。就绪通知在数据就绪时就生效,而异步IO直到数据IO完成才发生回调。
linux下的主流方案一直是就绪通知,其内核态异步IO方案甚至没有被封装到glibc
里去。围绕就绪通知,linux总共提出过三种解决方案。我们绕过select
和poll
方案,看看epoll
方案的特性。
另外提一点。有趣的是,当使用了epoll
后(更准确说只有在LT模式下),fd
是否为非阻塞其实已经不重要了。因为epoll保证每次去读取的时候都能读到数据,因此不会阻塞在调用上。
epoll
用户可以新建一个epoll
文件句柄,并且将其他fd
和这个"epoll fd
"关联。此后可以通过epoll fd
读取到所有就绪的文件句柄。
epoll
有两大模式,ET
和LT
。LT
模式下,每次读取就绪句柄都会读取出完整的就绪句柄。而ET
模式下,只给出上次到这次调用间新就绪的句柄。换个说法,如果ET
模式下某次读取出了一个句柄,这个句柄从未被读取完过——也就是从没有从就绪变为未就绪。那么这个句柄就永远不会被新的调用返回,哪怕上面其实充满了数据——因为句柄无法经历从非就绪变为就绪的过程。
类似CFS
,epoll
也使用了红黑树——不过是用于组织加入epoll
的所有fd
。epoll
的就绪列表使用的是双向队列。这方便系统将某个fd
加入队列中,或者从队列中解除。
要进一步了解epoll
的具体实现,可以参考linux下poll
和epoll
内核源码剖析。
性能
如果使用非阻塞函数,就不存在阻塞IO
导致上下文切换了,而是变为时间片耗尽被抢占(大部分情况下如此),因此读写的额外开销被消除。而epoll
的常规操作,都是O(1)
量级的。而epoll wait
的复制动作,则和当前需要返回的fd
数有关(在LT
模式下几乎就等同于上面的m,而ET
模式下则会大大减少)。
但是epoll
存在一点细节问题。epoll fd
的管理使用红黑树,因此在加入和删除时需要O(logn)
复杂度(n为总连接数),而且关联操作还必须每个fd
调用一次。因此在大连接量下频繁建立和关闭连接仍然有一定性能问题(超短连接)。不过关联操作调用毕竟比较少。如果确实是超短连接,tcp
连接和释放开销就很难接受了,所以对总体性能影响不大。
固有缺陷
原理上说,epoll
实现了一个wait_queue
的回调函数,因此原理上可以监听任何能够激活wait_queue
的对象。但是epoll
的最大问题是无法用于普通文件,因为普通文件始终是就绪的——虽然在读取的时候不是这样。
这导致基于epoll
的各种方案,一旦读到普通文件上下文仍然会阻塞。golang
为了解决这个问题,在每次调用syscall
的时候,会独立的启动一个线程,在独立的线程中进行调用。因此golang
在IO普通文件的时候网络不会阻塞。
事件通知机制下的几种程序设计模型
简述
使用通知机制的一大缺憾就是,用户进行IO操作后会陷入茫然——IO没有完成,所以当前上下文不能继续执行。但是由于复用线程的要求,当前线程还需要接着执行。所以,在如何进行异步编程上,又分化出数种方案。
用户态调度
首先需要知道的一点就是,异步编程大多数情况下都伴随着用户态调度问题——即使不使用上下文技术。
因为系统不会自动根据fd
的阻塞状况来唤醒合适的上下文了,所以这个工作必须由其他人——一般就是某种框架——来完成。
你可以想像一个fd
映射到对象的大map表,当我们从epoll
中得知某个fd
就绪后,需要唤醒某种对象,让他处理fd
对应的数据。
当然,实际情况会更加复杂一些。原则上所有不占用CPU时间的等待都需要中断执行,陷入睡眠,并且交由某种机构管理,等待合适的机会被唤醒。例如sleep
,或是文件IO
,还有lock
。更精确的说,所有在内核里面涉及到wait_queue
的,在框架里面都需要做这种机制——也就是把内核的调度和等待搬到用户态来。
当然,其实也有反过来的方案——就是把程序扔到内核里面去。其中最著名的实例大概是微软的http
服务器了。
这个所谓的“可唤醒可中断对象”,用的最多的就是协程。
协程
协程是一种编程组件,可以在不陷入内核的情况进行上下文切换。如此一来,我们就可以把协程上下文对象关联到fd
,让fd
就绪后协程恢复执行。
当然,由于当前地址空间和资源描述符的切换无论如何需要内核完成,因此协程所能调度的,只有在同一进程中的不同上下文而已。
如何做到
这是如何做到的呢?
我们在内核里实行上下文切换的时候,其实是将当前所有寄存器保存到内存中,然后从另一块内存中载入另一组已经被保存的寄存器。对于图灵机来说,当前状态寄存器意味着机器状态——也就是整个上下文。其余内容,包括栈上内存,堆上对象,都是直接或者间接的通过寄存器来访问的。
但是请仔细想想,寄存器更换这种事情,似乎不需要进入内核态么。事实上我们在用户态切换的时候,就是用了类似方案。
C coroutine
的实现,基本大多是保存现场和恢复之类的过程。Python
则是保存当前thread
的top frame(greenlet)
。
但是非常悲剧的,纯用户态方案(setjmp
/longjmp
)在多数系统上执行的效率很高,但是并不是为了协程而设计的。setjmp
并没有拷贝整个栈(大多数的coroutine
方案也不应该这么做),而是只保存了寄存器状态。这导致新的寄存器状态和老寄存器状态共享了同一个栈,从而在执行时互相破坏。而完整的coroutine
方案应当在特定时刻新建一个栈。
而比较好的方案(makecontext
/swapcontext
)则需要进入内核(sigprocmask
),这导致整个调用的性能非常低。
协程与线程的关系
首先我们可以明确,协程不能调度其他进程中的上下文。而后,每个协程要获得CPU,都必须在线程中执行。因此,协程所能利用的CPU数量,和用于处理协程的线程数量直接相关。
作为推论,在单个线程中执行的协程,可以视为单线程应用。这些协程,在未执行到特定位置(基本就是阻塞操作)前,是不会被抢占,也不会和其他CPU上的上下文发生同步问题的。因此,一段协程代码,中间没有可能导致阻塞的调用,执行在单个线程中。那么这段内容可以被视为同步的。
我们经常可以看到某些协程应用,一启动就是数个进程。这并不是跨进程调度协程。一般来说,这是将一大群fd分给多个进程,每个进程自己再做fd-协程对应调度。
基于就绪通知的协程框架
首先需要包装read/write,在发生read的时候检查返回。如果是EAGAIN,那么将当前协程标记为阻塞在对应fd上,然后执行调度函数。
调度函数需要执行epoll(或者从上次的返回结果缓存中取数据,减少内核陷入次数),从中读取一个就绪的fd。如果没有,上下文应当被阻塞到至少有一个fd就绪。
查找这个fd对应的协程上下文对象,并调度过去。
当某个协程被调度到时,他多半应当在调度器返回的路上——也就是read/write读不到数据的时候。因此应当再重试读取,失败的话返回1。
如果读取到数据了,直接返回。
这样,异步的数据读写动作,在我们的想像中就可以变为同步的。而我们知道同步模型会极大降低我们的编程负担。
CPS模型
其实这个模型有个更流行的名字——回调模型。之所以扯上CPS这么高大上的玩意,主要是里面涉及不少有趣的话题。
首先是回调模型的大致过程。在IO调用的时候,同时传入一个函数,作为返回函数。当IO结束时,调用传入的函数来处理下面的流程。这个模型听起来挺简单的。
然后是CPS。用一句话来描述这个模型——他把一切操作都当作了IO,无论干什么,结果要通过回调函数来返回。从这个角度来说,IO回调模型只能被视作CPS的一个特例。
例如,我们需要计算1+2*3,在cps里面就需要这么写:
mul(lambda x: add(pprint.pprint, x, 1), 2, 3)
其中mul和add在python里面如下定义:
add = lambda f, *nums: f(sum(nums))
mul = lambda f, *nums: f(reduce(lambda x,y: x*y, nums))
而且由于Python
没有TCO
,所以这样的写法会产生非常多的frame
。
但是要正确理解这个模型,你需要仔细思考一下以下几个问题:
- 函数的调用过程为什么必须是一个栈?
- IO过程在什么时间发生?调用发生时,还是回调时?
- 回调函数从哪里调用?如果当时利用工具去看上下文的话,调用栈是什么样子的?
函数组件和返回值
不知道你是否思考过为什么函数调用层级(上下文栈)会被表述为一个栈——是否有什么必要性,必须将函数调用的过程定义为一个栈呢?
原因就是返回值和同步顺序。对于大部分函数,我们需要得到函数计算的返回值。而要得到返回值,调用者就必须阻塞直到被调用者返回为止。因此调用者的执行状态就必须被保存,等到被调用者返回后继续——从这点来说,调用其实是最朴素的上下文切换手段。而对于少部分无需返回的函数,我们又往往需要他的顺序外部效应——例如干掉了某个进程,开了一个灯,或者仅仅是在环境变量里面添加了一项内容。而顺序外部效应同样需要等待被调用者返回以表明这个外部效应已经发生。
那么,如果我们不需要返回值也不需要顺序的外部效应呢?例如启动一个背景程序将数据发送到对端,无需保证发送成功的情况下。或者是开始一个数据抓取行为,无需保证抓取的成功。
通常这种需求我们就凑合着用一个同步调用混过去了——反正问题也不严重。但是对于阻塞相当严重的情况而言,很多人还是会考虑到将这个行为做成异步过程。目前最流行的异步调用分解工具就是mq——不仅异步,而且分布。当然,还有一个更简单的非分布方案——开一个coroutine。
而CPS则是另一个方向——函数的返回值可以不返回调用者,而是返回给第三者。
IO 过程在什么时间发生
其实这个问题的核心在于——整个回调模型是基于多路复用的还是基于异步IO的?
原则上两者都可以。你可以监听fd就绪,也可以监听IO完成。当然,即使监听IO完成,也不代表使用了内核态异步接口。很可能只是用epoll封装的而已。
回调函数的上下文环境
这个问题则需要和上面提到的“用户态调度框架”结合起来说。IO回调注册的实质是将回调函数绑定到某个fd上——就如同将coroutine绑定上去那样。只是coroutine允许你顺序的执行,而callback则会切碎函数。当然,大部分实现中,使用callback也有好处——coroutine的最小切换开销也在50ns,而call本身则只有2ns。
状态机模型
状态机模型是一个更难于理解和编程的模型,其本质是每次重入。
想像你是一个周期失忆的病人(就像“一周的朋友”那样)。那么你如何才能完成一项需要跨越周期的工作呢?例如刺绣,种植作物,或者——交一个男朋友。
当然,类比到失忆病人的例子上必须有一点限制。正常的生活技能,还有一些常识性的东西必须不能在周期失忆范围内。例如重新学习认字什么的可没人受的了。
答案就是——做笔记。每次重复失忆后,你需要阅读自己的笔记,观察上次做到哪个步骤,下一个步骤是什么。这需要将一个工作分解为很多步骤,在每个步骤内“重入”直到步骤完成,转移到下一个状态。
同理,在状态机模型解法里,每次执行都需要推演合适的状态,直到工作完成。这个模型已经很少用到了,因为相比回调函数来说,状态机模型更难理解和使用,性能差异也不大。
最后顺带一提,交一个男友的方案和其他几个略有不同,主要靠颜好高冷反差萌,一般人就不要尝试挑战了。。。当然一般人也不会一周失忆一次,毕竟生活不是韩剧也不是日本动漫。。。
博主注解
LinkedHashMap源码解析
概述
Hash table and linked list implementation of the Map interface, with predictable iteration order. This implementation differs fromHashMap in that it maintains a doubly-linked list running through all of its entries. This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order). Note that insertion order is not affected if a key is re-inserted into the map. (A key k is reinserted into a map m if m.put(k, v) is invoked when m.containsKey(k) would return trueimmediately prior to the invocation.)
其header
头定义
/**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header;
/**
* Called by superclass constructors and pseudoconstructors (clone,
* readObject) before any entries are inserted into the map. Initializes
* the chain.
*/
@Override
void init() {
header = new Entry<>(-1, null, null, null);
header.before = header.after = header;
}
put
方法:
LinkedHashMap的Hash算法是沿用了HashMap的算法。其没有实现put
方法,实现了get
方法为了方便accessOrder
方法。
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
if (key == null)
return putForNullKey(value); // 对于null的特殊处理
int hash = hash(key);
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this); //这里是Entry自己实现的,HashMap中保留为空方法
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
因为其Entry是一个双向链表,其中定义是
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
我们先看一下不需要按照访问顺序进行排序的(即accessOrder = false
)的情况:
每个元素如果没有冲突都会执行addEntry
方法
addEntry
与createEntry
/**
* This override alters behavior of superclass put method. It causes newly
* allocated entry to get inserted at the end of the linked list and
* removes the eldest entry if appropriate.
*/
void addEntry(int hash, K key, V value, int bucketIndex) {
super.addEntry(hash, key, value, bucketIndex);
// Remove eldest entry if instructed
Entry<K,V> eldest = header.after;
if (removeEldestEntry(eldest)) { // 始终返回false
removeEntryForKey(eldest.key);
}
}
/**
* This override differs from addEntry in that it doesn't resize the
* table or remove the eldest entry.
*/
void createEntry(int hash, K key, V value, int bucketIndex) {
HashMap.Entry<K,V> old = table[bucketIndex];
Entry<K,V> e = new Entry<>(hash, key, value, old);
table[bucketIndex] = e;
e.addBefore(header);
size++;
}
Entry:
/**
* Inserts this entry before the specified existing entry in the list.
*/
private void addBefore(Entry<K,V> existingEntry) {
after = existingEntry;
before = existingEntry.before;
before.after = this;
after.before = this;
}
在执行super.addEntry
的时候回执行createEntry
方法,此时最主要的区别就是addBefore
方法,此方法是将自己这个新增的Entry放置在existingEntry
之后,也就是其头结点header
之后。可以通过 header
来遍历这个双向链表。
再分析需要按照访问顺序进行排序的(即accessOrder = false
)的情况:
此时put方法中如果key原本就是有值,即相当于我们队access进行了一次访问,此时执行recordAccess
方法
void recordAccess(HashMap<K,V> m) {
LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
if (lm.accessOrder) {
lm.modCount++;
remove(); // 先将该节点双向链表中删除
addBefore(lm.header); // 再将该结点防止与header之后
}
}
/**
* Removes this entry from the linked list.
*/
private void remove() {
before.after = after;
after.before = before;
}
get
方法:
public V get(Object key) {
Entry<K,V> e = (Entry<K,V>)getEntry(key);
if (e == null)
return null;
e.recordAccess(this);
return e.value;
}
此处可同理上述,访问时候执行recordAccess
然后根据accessOrder
的值决定其行为。
Spring源码分析-bean的解析(2)
Spring源码分析-bean的解析(2)
当前版本 Spring 4.3.8
默认标签的解析
接上 parseDefaultElement(ele, delegate);
DefaultBeanDefinitionDocumentReader#parseDefaultElement
private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
// import 标签解析
if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
importBeanDefinitionResource(ele);
}
// 解析 alias 标签
else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
processAliasRegistration(ele);
}
// bean 的解析
else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
processBeanDefinition(ele, delegate);
}
// beans 的解析
else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
// recurse
doRegisterBeanDefinitions(ele);
}
}
Bean 标签的解析及注册
/**
* Process the given bean element, parsing the bean definition
* and registering it with the registry.
*/
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); // 1
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); // 2
try {
// Register the final decorated instance.
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); // 3
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); // 4
}
}
- 首先委托
BeanDefinitionDelegate
类的parseBeanDefinitionElement
方法进行元素解析,返回BeanDefinitionHolder
类型的实例bdHolder
,经过这个方法后,bdHolder
实例已经包含我们的配置文件中配置的各个属性了,例如class、name、id、alias
之类的属性。 - 当返回的
bdHolder
不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析。 - 解析完成之后,需要对解析后的
bdHolder
进行注册,同样,注册操作委托给了BeanDefinitionReaderUtils
的registerBeanDefinition
方法。 - 最后发出响应事件,通知想关的监听器,这个
bean
已经加载完成了
解析 BeanDefinition
BeanDefinitionDelegate#parseBeanDefinitionElement
/**
* Parses the supplied {@code <bean>} element. May return {@code null}
* if there were errors during parse. Errors are reported to the
* {@link org.springframework.beans.factory.parsing.ProblemReporter}.
*/
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) {
return parseBeanDefinitionElement(ele, null);
}
/**
* Parses the supplied {@code <bean>} element. May return {@code null}
* if there were errors during parse. Errors are reported to the
* {@link org.springframework.beans.factory.parsing.ProblemReporter}.
*/
public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
// 1
// 解析 id 属性
String id = ele.getAttribute(ID_ATTRIBUTE);
// 解析 name 属性
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
// 分割 name 属性
List<String> aliases = new ArrayList<String>();
if (StringUtils.hasLength(nameAttr)) {
String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS);
aliases.addAll(Arrays.asList(nameArr));
}
String beanName = id;
if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
beanName = aliases.remove(0);
if (logger.isDebugEnabled()) {
logger.debug("No XML 'id' specified - using '" + beanName +
"' as bean name and " + aliases + " as aliases");
}
}
if (containingBean == null) {
checkNameUniqueness(beanName, aliases, ele);
}
AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); // 2
if (beanDefinition != null) {
if (!StringUtils.hasText(beanName)) { // 3
// 如果不存在 beanName 那么根据 Spring 中提供的命名规则为当前 bean 生成对应的 beanName
try {
if (containingBean != null) {
beanName = BeanDefinitionReaderUtils.generateBeanName(
beanDefinition, this.readerContext.getRegistry(), true);
}
else {
beanName = this.readerContext.generateBeanName(beanDefinition);
// Register an alias for the plain bean class name, if still possible,
// if the generator returned the class name plus a suffix.
// This is expected for Spring 1.2/2.0 backwards compatibility.
String beanClassName = beanDefinition.getBeanClassName();
if (beanClassName != null &&
beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
!this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
aliases.add(beanClassName);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Neither XML 'id' nor 'name' specified - " +
"using generated bean name [" + beanName + "]");
}
}
catch (Exception ex) {
error(ex.getMessage(), ele);
return null;
}
}
String[] aliasesArray = StringUtils.toStringArray(aliases);
return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); // 4
}
return null;
}
- 提取元素中的 id 以及 name 属性
- 进一步解析其他所有属性并统一封装至
GenericBeanDefinition
类型的实例中(下面的函数) - 如果检测到 bean 没有指定 beanName ,那么使用默认规则为此 Bean 生成 beanName
- 将获取到的信息封装到 BeanDefinitionHolder 的实例中
/*parseBeanDefinitionElement*/
/**
* Parse the bean definition itself, without regard to name or aliases. May return
* {@code null} if problems occurred during the parsing of the bean definition.
*/
public AbstractBeanDefinition parseBeanDefinitionElement(
Element ele, String beanName, BeanDefinition containingBean) {
this.parseState.push(new BeanEntry(beanName));
String className = null;
if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
}
try {
String parent = null;
// 解析parent 属性
if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
parent = ele.getAttribute(PARENT_ATTRIBUTE);
}
// 创建用于承载属性的 AbstractBeanDefinition 类型的 GenericBeanDefinition
AbstractBeanDefinition bd = createBeanDefinition(className, parent);
// 硬编码解析默认 bean 的各种属性
parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
// 提取description
bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
// 解析元数据
parseMetaElements(ele, bd);
// 解析 lookup-override 属性
parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
// 解析 replaced-method 属性
parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
// 解析构造函数参数
parseConstructorArgElements(ele, bd);
// 解析 property 子元素
parsePropertyElements(ele, bd);
// 解析 qualifier 子元素
parseQualifierElements(ele, bd);
bd.setResource(this.readerContext.getResource());
bd.setSource(extractSource(ele));
return bd;
}
catch (ClassNotFoundException ex) {
error("Bean class [" + className + "] not found", ele, ex);
}
catch (NoClassDefFoundError err) {
error("Class that bean class [" + className + "] depends on not found", ele, err);
}
catch (Throwable ex) {
error("Unexpected failure during bean definition parsing", ele, ex);
}
finally {
this.parseState.pop();
}
return null;
}
接下来来看看一些复杂标签的解析:
创建用于属性承载的 BeanDefinition
BeanDefinition
是一个接口,在 Spring 中存在三种实现,三种实现均继承了 AbstactBeanDefinition
,其中 BeanDefinition
是配置文件中的 <bean>
元素标签在容器中的内部表示形式。<bean>
元素标签拥有 class、scope、lazy-init
等配置属性。BeanDefinition
则提供了相应的 beanClass、scope、lazyInit
属性,BeanDefinition
和 <bean>
中的属性是一一对应的。
-
RootBeanDefinition
最常用的实现类,它对应一般性的<bean>
元素标签当前版本中,
ConfigurationClassBeanDefinition
继承与此类 -
ChildBeanDefinition
配置文件中的子<bean>
-
GenericBeanDefinition
2.5 新加入的bean
文件配置属性定义类,是一站式服务类。在当前版本中,有
ScannedGenericBeanDefinition
,AnnotatedGenericBeanDefinition
继承与此类
Spring 通过 BeanDefinition
将配置文件中的 <bean>
配置信息转换为容器的内部表示,并将这些 BeanDefinition
注册到 BeanDefinitionRegistry
中。Spring 容器的 BeanDefinitionRegistry
就像是 Spring 配置信息的内存数据库,主要是以 map 的形式保存的,后续操作直接从 BeanDefinitionRegistry
中读取配置信息。
BeanDefinitionParserDelegate#createBeanDefinition
/**
* Create a bean definition for the given class name and parent name.
* @param className the name of the bean class
* @param parentName the name of the bean's parent bean
* @return the newly created bean definition
* @throws ClassNotFoundException if bean class resolution was attempted but failed
*/
protected AbstractBeanDefinition createBeanDefinition(String className, String parentName)
throws ClassNotFoundException {
return BeanDefinitionReaderUtils.createBeanDefinition(
parentName, className, this.readerContext.getBeanClassLoader());
}
/**
* Create a new GenericBeanDefinition for the given parent name and class name,
* eagerly loading the bean class if a ClassLoader has been specified.
* @param parentName the name of the parent bean, if any
* @param className the name of the bean class, if any
* @param classLoader the ClassLoader to use for loading bean classes
* (can be {@code null} to just register bean classes by name)
* @return the bean definition
* @throws ClassNotFoundException if the bean class could not be loaded
*/
public static AbstractBeanDefinition createBeanDefinition(
String parentName, String className, ClassLoader classLoader) throws ClassNotFoundException {
GenericBeanDefinition bd = new GenericBeanDefinition();
// parentName 可能为空
bd.setParentName(parentName);
if (className != null) {
if (classLoader != null) {
// 如果 classLoader 不为空,则使用已传入的 classLoader 同一虚拟机加载类对象,否则只是记录 className
bd.setBeanClass(ClassUtils.forName(className, classLoader));
}
else {
bd.setBeanClassName(className);
}
}
return bd;
}
解析各种属性
/**
* Apply the attributes of the given bean element to the given bean * definition.
* @param ele bean declaration element
* @param beanName bean name
* @param containingBean containing bean definition
* @return a bean definition initialized according to the bean element attributes
*/
public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName,
BeanDefinition containingBean, AbstractBeanDefinition bd) {
// 解析 singleton 属性。已废弃,使用 scope 设置
if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) {
error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele);
}
// 解析 scope 属性
// https://docs.spring.io/spring/docs/5.0.0.RELEASE/spring-framework-reference/core.html#beans-factory-scopes
else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) {
bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE));
}
else if (containingBean != null) {
// Take default from containing bean in case of an inner bean definition.
// 在嵌入 beanDefinition 情况下且没有单独指定 scope 属性则使用父类默认的属性
bd.setScope(containingBean.getScope());
}
// 解析 absract 属性
// (抽象bean不能被实例化,只能被子类继承)。
if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) {
bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE)));
}
// 解析 lazy-init 属性
// 懒初始化,默认false ,即:非懒惰初始化。这是高效的,可以提前暴露错误(如果存在),如果是懒初始化,只有在call 时才初始化。
String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE);
if (DEFAULT_VALUE.equals(lazyInit)) {
lazyInit = this.defaults.getLazyInit();
}
// 若没有设置或设置成其他字符都会被设置为 false
bd.setLazyInit(TRUE_VALUE.equals(lazyInit));
// 解析 autowire 属性
String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE);
bd.setAutowireMode(getAutowireMode(autowire));
// 解析 dependency-check 属性
String dependencyCheck = ele.getAttribute(DEPENDENCY_CHECK_ATTRIBUTE);
bd.setDependencyCheck(getDependencyCheck(dependencyCheck));
// 解析 depends-on 属性
// 初始化该bean之前必须先初始化谁(name or id 指定)
if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) {
String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE);
bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS));
}
// 解析 autowire-candidate 属性
String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE);
if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) {
String candidatePattern = this.defaults.getAutowireCandidates();
if (candidatePattern != null) {
String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern);
bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName));
}
}
else {
bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate));
}
// 解析 primary 属性
if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) {
bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE)));
}
// 解析 init-method 属性
if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) {
String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE);
if (!"".equals(initMethodName)) {
bd.setInitMethodName(initMethodName);
}
}
else {
if (this.defaults.getInitMethod() != null) {
bd.setInitMethodName(this.defaults.getInitMethod());
bd.setEnforceInitMethod(false);
}
}
// 解析 destory-method 属性
if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) {
String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE);
bd.setDestroyMethodName(destroyMethodName);
}
else {
if (this.defaults.getDestroyMethod() != null) {
bd.setDestroyMethodName(this.defaults.getDestroyMethod());
bd.setEnforceDestroyMethod(false);
}
}
// 解析 factory-method 属性
if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) {
bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE));
}
// 解析 factory-bean 属性
if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) {
bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE));
}
return bd;
}
简单复习一下 bean 的自动装配 @Autowired (如果设置参数 require = false 则会尝试匹配,无匹配这让 bean 处于未装配状态)
- primary(如果配置了的)
- @qualifier 做限定
- 配合 @Autowired 使用指定需要装配哪个 bean
- 配合 @component / @bean 指定限定符,不然在 @Autowired 使用的时候就是默认 beanId
- 符合的类型
解析子元素 meta
meta: 元数据,当需要使用里面的信息时可以通过key获取
Finally, the bean definitions should contain matching qualifier values. This example also demonstrates that bean meta attributes may be used instead of the
<qualifier/>
sub-elements.If available, the<qualifier/>
and its attributes would take precedence, but the autowiring mechanism will fallback on the values provided within the<meta/>
tags if no such qualifier is present
<bean id="myTestBean" class="bean.MyTestBean">
<meta key="testStr" value="test">
</bean>
public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) {
// 获取当前节点的所有子元素
NodeList nl = ele.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 提取 meta
if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) {
Element metaElement = (Element) node;
String key = metaElement.getAttribute(KEY_ATTRIBUTE);
String value = metaElement.getAttribute(VALUE_ATTRIBUTE);
BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value);
attribute.setSource(extractSource(metaElement));
attributeAccessor.addMetadataAttribute(attribute);
}
}
}
解析子元素 lookup-method
获取器注入,特殊方法注入,它是把一个方法声明为返回某种类型的 bean
, 但实际要返回的 bean
是在配置文件里面配置的,此方法可用在设计有些可插拔的功能上,解除程序依赖。
简单来说就是,可以动态配置某个方法的返回值返回的 bean
... <lookup-method name="方法名" bean="需要方法返回的 beanId"> ...
/**
* Parse lookup-override sub-elements of the given bean element.
*/
public void parseLookupOverrideSubElements(Element beanEle, MethodOverrides overrides) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 仅当在 Spring 默认 bean 的子元素且为 <lookup-method 时有效
if (isCandidateElement(node) && nodeNameEquals(node, LOOKUP_METHOD_ELEMENT)) {
Element ele = (Element) node;
// 获取要修饰的方法
String methodName = ele.getAttribute(NAME_ATTRIBUTE);
// 获取配置返回的 bean
String beanRef = ele.getAttribute(BEAN_ELEMENT);
LookupOverride override = new LookupOverride(methodName, beanRef);
override.setSource(extractSource(ele));
overrides.addOverride(override);
}
}
}
解析子元素 replaced-method
方法替换:可以在运行时用新的方法替换现有的方法。与之前的 look-up
不同的是,replaced-method
不但可以动态地替换返回实体 bean
,而且还能动态地更改原有方法的逻辑。
需要实现 MethodReplacer
的 reimplement
方法,该方法替换其指定方法。
/**
* Parse replaced-method sub-elements of the given bean element.
*/
public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 识别标签
if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) {
Element replacedMethodEle = (Element) node;
// 提取需要替换方法
String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE);
// 提权对应的新的替换方法
String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE);
ReplaceOverride replaceOverride = new ReplaceOverride(name, callback);
// Look for arg-type match elements.
List<Element> argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT);
for (Element argTypeEle : argTypeEles) {
// 记录参数
String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE);
match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle));
if (StringUtils.hasText(match)) {
replaceOverride.addTypeIdentifier(match);
}
}
replaceOverride.setSource(extractSource(replacedMethodEle));
overrides.addOverride(replaceOverride);
}
}
}
解析子元素 constructor-arg
/**
* Parse a constructor-arg element.
*/
public void parseConstructorArgElement(Element ele, BeanDefinition bd) {
// 提取对应元素
String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE);
String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE);
String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(indexAttr)) {
try {
int index = Integer.parseInt(indexAttr);
if (index < 0) {
error("'index' cannot be lower than 0", ele);
}
else {
try {
this.parseState.push(new ConstructorArgumentEntry(index));
// 解析 ele 对应的属性元素
Object value = parsePropertyValue(ele, bd, null);
// 使用 ConstructorArgumentValues.ValueHolder 类型封装解析出来的元素
ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
if (StringUtils.hasLength(typeAttr)) {
valueHolder.setType(typeAttr);
}
if (StringUtils.hasLength(nameAttr)) {
valueHolder.setName(nameAttr);
}
valueHolder.setSource(extractSource(ele));
// 不允许重复指定相同的参数
if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) {
error("Ambiguous constructor-arg entries for index " + index, ele);
}
else {
// 将封装的信息添加到当前 BeanDefinition 的 constructorArgumentValues 中的 indexedArgumentValue 属性中
bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder);
}
}
finally {
this.parseState.pop();
}
}
}
catch (NumberFormatException ex) {
error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele);
}
}
else {
// 没有 index 属性则忽略去属性,自动寻找
try {
this.parseState.push(new ConstructorArgumentEntry());
// 解析 constructor-arg 元素
Object value = parsePropertyValue(ele, bd, null);
ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value);
if (StringUtils.hasLength(typeAttr)) {
valueHolder.setType(typeAttr);
}
if (StringUtils.hasLength(nameAttr)) {
valueHolder.setName(nameAttr);
}
valueHolder.setSource(extractSource(ele));
bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder);
}
finally {
this.parseState.pop();
}
}
}
解析构造函数配置中子元素的过程
/**
* Get the value of a property element. May be a list etc.
* Also used for constructor arguments, "propertyName" being null in this case.
*/
public Object parsePropertyValue(Element ele, BeanDefinition bd, String propertyName) {
String elementName = (propertyName != null) ?
"<property> element for property '" + propertyName + "'" :
"<constructor-arg> element";
// Should only have one child element: ref, value, list, etc.
// 一个属性只能对应一种类型:ref、value、list等
NodeList nl = ele.getChildNodes();
Element subElement = null;
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
// 对应 description 或者 meta 不处理
if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) &&
!nodeNameEquals(node, META_ELEMENT)) {
// Child element is what we're looking for.
if (subElement != null) {
error(elementName + " must not contain more than one sub-element", ele);
}
else {
subElement = (Element) node;
}
}
}
// 解析 constructor-arg 上的 ref 属性
boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE);
// 解析 constructor-arg 上的 value 属性
boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE);
if ((hasRefAttribute && hasValueAttribute) ||
((hasRefAttribute || hasValueAttribute) && subElement != null)) {
/**
在 constructor-arg 上不存在:
1. 同时既有 ref 属性又有 value 属性
2. 存在 ref 属性或者 value 属性且又有子元素
**/
error(elementName +
" is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele);
}
if (hasRefAttribute) {
// ref 属性的处理,使用 RuntimeBeanRefence 封装对应的 ref 名称
String refName = ele.getAttribute(REF_ATTRIBUTE);
if (!StringUtils.hasText(refName)) {
error(elementName + " contains empty 'ref' attribute", ele);
}
RuntimeBeanReference ref = new RuntimeBeanReference(refName);
ref.setSource(extractSource(ele));
return ref;
}
else if (hasValueAttribute) {
// value 属性的处理,使用 TypedStringValue 封装
TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE));
valueHolder.setSource(extractSource(ele));
return valueHolder;
}
else if (subElement != null) {
// 解析子元素
return parsePropertySubElement(subElement, bd);
}
else {
// Neither child element nor "ref" or "value" attribute found.
// 即没有 ref 也没有 value 也没有子元素,报错
error(elementName + " must specify a ref or value", ele);
return null;
}
}
所谓子元素
<constructor-arg>
<map>
<entry key="key" value="value"/>
</map>
</constructor-arg>
public Object parsePropertySubElement(Element ele, BeanDefinition bd) {
return parsePropertySubElement(ele, bd, null);
}
/**
* Parse a value, ref or collection sub-element of a property or
* constructor-arg element.
* @param ele subelement of property element; we don't know which yet
* @param defaultValueType the default type (class name) for any
* {@code <value>} tag that might be created
*/
public Object parsePropertySubElement(Element ele, BeanDefinition bd, String defaultValueType) {
if (!isDefaultNamespace(ele)) {
return parseNestedCustomElement(ele, bd);
}
else if (nodeNameEquals(ele, BEAN_ELEMENT)) {
BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd);
if (nestedBd != null) {
nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd);
}
return nestedBd;
}
else if (nodeNameEquals(ele, REF_ELEMENT)) {
// A generic reference to any name of any bean.
String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE);
boolean toParent = false;
if (!StringUtils.hasLength(refName)) {
// 解析 local
// A reference to the id of another bean in the same XML file.
refName = ele.getAttribute(LOCAL_REF_ATTRIBUTE);
if (!StringUtils.hasLength(refName)) {
// 解析 parent
// A reference to the id of another bean in a parent context.
refName = ele.getAttribute(PARENT_REF_ATTRIBUTE);
toParent = true;
if (!StringUtils.hasLength(refName)) {
error("'bean', 'local' or 'parent' is required for <ref> element", ele);
return null;
}
}
}
if (!StringUtils.hasText(refName)) {
error("<ref> element contains empty target attribute", ele);
return null;
}
RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent);
ref.setSource(extractSource(ele));
return ref;
}
// 对 idref 元素的解析
else if (nodeNameEquals(ele, IDREF_ELEMENT)) {
return parseIdRefElement(ele);
}
// 对 value 子元素的解析
else if (nodeNameEquals(ele, VALUE_ELEMENT)) {
return parseValueElement(ele, defaultValueType);
}
// 对 null 子元素的解析
else if (nodeNameEquals(ele, NULL_ELEMENT)) {
// It's a distinguished null value. Let's wrap it in a TypedStringValue
// object in order to preserve the source location.
TypedStringValue nullHolder = new TypedStringValue(null);
nullHolder.setSource(extractSource(ele));
return nullHolder;
}
// 解析 array 子元素
else if (nodeNameEquals(ele, ARRAY_ELEMENT)) {
return parseArrayElement(ele, bd);
}
// 解析 list 子元素
else if (nodeNameEquals(ele, LIST_ELEMENT)) {
return parseListElement(ele, bd);
}
// 解析 set 子元素
else if (nodeNameEquals(ele, SET_ELEMENT)) {
return parseSetElement(ele, bd);
}
// 解析 map 子元素
else if (nodeNameEquals(ele, MAP_ELEMENT)) {
return parseMapElement(ele, bd);
}
// 解析 parse 子元素
else if (nodeNameEquals(ele, PROPS_ELEMENT)) {
return parsePropsElement(ele);
}
else {
error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele);
return null;
}
}
解析子元素 property
/**
* Parse property sub-elements of the given bean element.
*/
public void parsePropertyElements(Element beanEle, BeanDefinition bd) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) {
parsePropertyElement((Element) node, bd);
}
}
}
/**
* Parse a property element.
*/
public void parsePropertyElement(Element ele, BeanDefinition bd) {
// 获取配置元素中 name 的值
String propertyName = ele.getAttribute(NAME_ATTRIBUTE);
if (!StringUtils.hasLength(propertyName)) {
error("Tag 'property' must have a 'name' attribute", ele);
return;
}
this.parseState.push(new PropertyEntry(propertyName));
try {
// 不允许多次对同一属性配置
if (bd.getPropertyValues().contains(propertyName)) {
error("Multiple 'property' definitions for property '" + propertyName + "'", ele);
return;
}
Object val = parsePropertyValue(ele, bd, propertyName);
PropertyValue pv = new PropertyValue(propertyName, val);
parseMetaElements(ele, pv);
pv.setSource(extractSource(ele));
bd.getPropertyValues().addPropertyValue(pv);
}
finally {
this.parseState.pop();
}
}
解析子元素 qualifier
/**
* Parse qualifier sub-elements of the given bean element.
*/
public void parseQualifierElements(Element beanEle, AbstractBeanDefinition bd) {
NodeList nl = beanEle.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ELEMENT)) {
parseQualifierElement((Element) node, bd);
}
}
}
/**
* Parse a qualifier element.
*/
public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) {
String typeName = ele.getAttribute(TYPE_ATTRIBUTE);
if (!StringUtils.hasLength(typeName)) {
error("Tag 'qualifier' must have a 'type' attribute", ele);
return;
}
this.parseState.push(new QualifierEntry(typeName));
try {
AutowireCandidateQualifier qualifier = new AutowireCandidateQualifier(typeName);
qualifier.setSource(extractSource(ele));
String value = ele.getAttribute(VALUE_ATTRIBUTE);
if (StringUtils.hasLength(value)) {
qualifier.setAttribute(AutowireCandidateQualifier.VALUE_KEY, value);
}
NodeList nl = ele.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (isCandidateElement(node) && nodeNameEquals(node, QUALIFIER_ATTRIBUTE_ELEMENT)) {
Element attributeEle = (Element) node;
String attributeName = attributeEle.getAttribute(KEY_ATTRIBUTE);
String attributeValue = attributeEle.getAttribute(VALUE_ATTRIBUTE);
if (StringUtils.hasLength(attributeName) && StringUtils.hasLength(attributeValue)) {
BeanMetadataAttribute attribute = new BeanMetadataAttribute(attributeName, attributeValue);
attribute.setSource(extractSource(attributeEle));
qualifier.addMetadataAttribute(attribute);
}
else {
error("Qualifier 'attribute' tag must have a 'name' and 'value'", attributeEle);
return;
}
}
}
bd.addQualifier(qualifier);
}
finally {
this.parseState.pop();
}
}
AbstractBeanDefinition 属性
public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor
implements BeanDefinition, Cloneable {
// 忽略常量
private volatile Object beanClass;
// bean 的作用范围,对应 bean 属性 scope
private String scope = SCOPE_DEFAULT;
// 是否是抽象,对应 abstract
private boolean abstractFlag = false;
// 是否延迟加载,对应 lazy-init
private boolean lazyInit = false;
// 自动注入模式,对应 autowire
private int autowireMode = AUTOWIRE_NO;
// 依赖检查, 3.0 弃用
private int dependencyCheck = DEPENDENCY_CHECK_NONE;
// 用来表示一个 bean 的实例化依靠另一个 bean 先实例化,对应 depend-on
private String[] dependsOn;
// autowire-candidate 属性设置为 false,这样容器在查找自动装配对象时,
// 将不考虑该 bean,即它不会被考虑作为其他 bean 自动装配的候选者,但是该 bean 本身还是可以
// 使用自动装配来注入其他 bean 的。
// 对应 bean 属性 autowire-candidate
private boolean autowireCandidate = true;
// 自动装配时当出现多个 bean 候选者时,将作为首选者,对应 primary
private boolean primary = false;
// 用于记录 Qualifier,对应子元素 qualifier
private final Map<String, AutowireCandidateQualifier> qualifiers =
new LinkedHashMap<String, AutowireCandidateQualifier>(0);
// 允许访问非公开的构造器和方法,程序设置
private boolean nonPublicAccessAllowed = true;
/**
是否以一种宽松模式解析构造函数,默认为 true
如果为 false,则在如下情况
interface ITest{}
class ITestImpl implements ITest{};
class Main{
Main(ITest i) {}
Main(ITestImpl i) {}
}
抛出异常,因为 Spring 无法准确定位哪个构造函数
程序设置
**/
private boolean lenientConstructorResolution = true;
/**
对应 bean 属性 factory-bean
<bean id="instanceFactoryBean" class="xxx"/>
<bean id="currentTime" factory-bean="instanceFactoryBean" factory-method="createTime"/>
**/
private String factoryBeanName;
// 对应 factory-method
private String factoryMethodName;
// 记录构造函数注入属性,对于 constructor-arg
private ConstructorArgumentValues constructorArgumentValues;
// 普通属性集合
private MutablePropertyValues propertyValues;
// 方法重写的持有者,记录 lookup-method、replaced-method 元素
private MethodOverrides methodOverrides = new MethodOverrides();
// 初始化方法,对应 init-method
private String initMethodName;
// 销毁方法,对应 destory-method
private String destroyMethodName;
// 是否执行 init-method,程序设定
private boolean enforceInitMethod = true;
// 是否执行 destory-method,程序设置
private boolean enforceDestroyMethod = true;
// 是否是用户定义的而不是应用程序本身定义的,创建 AOP 时候为 true,程序设置
private boolean synthetic = false;
/**
定义这个 bean 的应用
APPLICATION: 用户
INFRASTRUCTURE:完全内部使用,与用户无光
SUPPORT: 某些复杂配置的一部分
程序设置
**/
private int role = BeanDefinition.ROLE_APPLICATION;
// bean 的描述信息
private String description;
// 这个 bean 定义的资源
private Resource resource;
// ...各种 set/get
}
解析默认表情中的自定义标签元素
之前的解析标签起始函数:
/**
* Process the given bean element, parsing the bean definition
* and registering it with the registry.
*/
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); // 1
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); // 2
try {
// Register the final decorated instance.
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); // 3
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to register bean definition with name '" +
bdHolder.getBeanName() + "'", ele, ex);
}
// Send registration event.
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); // 4
}
}
接下来介绍 2 bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
适用场景:
<bean id="test" class="test.MyClass">
<mybean:user username="aaa" />
</bean>
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) {
return decorateBeanDefinitionIfRequired(ele, definitionHolder, null);
}
// containingBd 为父类的 bean,为了使用父类的 scope 属性
public BeanDefinitionHolder decorateBeanDefinitionIfRequired(
Element ele, BeanDefinitionHolder definitionHolder, BeanDefinition containingBd) {
BeanDefinitionHolder finalDefinition = definitionHolder;
// Decorate based on custom attributes first.
NamedNodeMap attributes = ele.getAttributes();
// 遍历所有的属性,看看是否有适用于修饰的属性
for (int i = 0; i < attributes.getLength(); i++) {
Node node = attributes.item(i);
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
// Decorate based on custom nested elements.
NodeList children = ele.getChildNodes();
// 遍历所有的子节点,看看是否有使用于修饰的子元素
for (int i = 0; i < children.getLength(); i++) {
Node node = children.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
finalDefinition = decorateIfRequired(node, finalDefinition, containingBd);
}
}
return finalDefinition;
}
public BeanDefinitionHolder decorateIfRequired(
Node node, BeanDefinitionHolder originalDef, BeanDefinition containingBd) {
// 获取自定义标签的命名空间
String namespaceUri = getNamespaceURI(node);
// 对于非默认标签进行修饰
if (!isDefaultNamespace(namespaceUri)) {
// 根据命名空间找到对应的处理器
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler != null) {
// 进行修饰
return handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd));
}
else if (namespaceUri != null && namespaceUri.startsWith("http://www.springframework.org/")) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node);
}
else {
// A custom namespace, not to be handled by Spring - maybe "xml:...".
if (logger.isDebugEnabled()) {
logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]");
}
}
}
return originalDef;
}
注册解析的 BeanDefinition
/**
* Register the given bean definition with the given bean factory.
* @param definitionHolder the bean definition including name and aliases
* @param registry the bean factory to register with
* @throws BeanDefinitionStoreException if registration failed
*/
public static void registerBeanDefinition(
BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
throws BeanDefinitionStoreException {
// Register bean definition under primary name.
// 使用 beanName 做唯一标识注册
String beanName = definitionHolder.getBeanName();
registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());
// Register aliases for bean name, if any.
// 注册所有的别名
String[] aliases = definitionHolder.getAliases();
if (aliases != null) {
for (String alias : aliases) {
registry.registerAlias(beanName, alias);
}
}
}
通过 beanName 注册 BeanDefinition
// DefaultListableBeanFactory
@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
Assert.hasText(beanName, "Bean name must not be empty");
Assert.notNull(beanDefinition, "BeanDefinition must not be null");
if (beanDefinition instanceof AbstractBeanDefinition) {
try {
/**
注册前的最后一次校验,这里的校验不同于之前的 XML 文件校验,
主要是对于 AbstractBeanDefinition 属性中的 methodOverrides 校验,
校验 methodOverrides 是否与工厂方法并存或者 methodOverrides 对应的方法根本不存在
**/
((AbstractBeanDefinition) beanDefinition).validate();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Validation of bean definition failed", ex);
}
}
BeanDefinition oldBeanDefinition;
// 这个 beanDefinitionMap 是 ConcurrentHashMap,考虑并发
oldBeanDefinition = this.beanDefinitionMap.get(beanName);
if (oldBeanDefinition != null) {
// 如果对应的 BeanName 已经注册且在配置中配置了 bean 不允许被覆盖,则抛出异常
if (!isAllowBeanDefinitionOverriding()) {
throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
"Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
"': There is already [" + oldBeanDefinition + "] bound.");
}
else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (this.logger.isWarnEnabled()) {
this.logger.warn("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
oldBeanDefinition + "] with [" + beanDefinition + "]");
}
}
else if (!beanDefinition.equals(oldBeanDefinition)) {
if (this.logger.isInfoEnabled()) {
this.logger.info("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + oldBeanDefinition +
"] with [" + beanDefinition + "]");
}
}
else {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + oldBeanDefinition +
"] with [" + beanDefinition + "]");
}
}
// 注册 beanDefinition
this.beanDefinitionMap.put(beanName, beanDefinition);
}
// 不属于 AbstractBeanDefinition 了
// 疑问,什么不属于 AbstactBeanDefinition 的 BeanDefinition 的实现?
else {
//
if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
// 这里为什么还需要锁?
synchronized (this.beanDefinitionMap) {
this.beanDefinitionMap.put(beanName, beanDefinition);
List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
updatedDefinitions.addAll(this.beanDefinitionNames);
updatedDefinitions.add(beanName);
// 这里需要锁!
this.beanDefinitionNames = updatedDefinitions;
if (this.manualSingletonNames.contains(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
updatedSingletons.remove(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// Still in startup registration phase
this.beanDefinitionMap.put(beanName, beanDefinition);
// 记录 beanName
this.beanDefinitionNames.add(beanName);
this.manualSingletonNames.remove(beanName);
}
this.frozenBeanDefinitionNames = null;
}
if (oldBeanDefinition != null || containsSingleton(beanName)) {
// 清楚解析之前留下的对应 beanName 的缓存
resetBeanDefinition(beanName);
}
}
- 对
AbstractBeanDefinition
的校验。在解析 XML 文件的时候我们提过校验,但是此校验非彼校验,之前的校验时针对于 XML 格式的校验,而此时的校验时针是对于AbstractBeanDefinition
的methodOverrides
属性的。 - 对
beanName
已经注册的情况的处理。如果设置了不允许bean
的覆盖,则需要抛出异常,否则直接覆盖 - 加入 map 缓存
- 清除解析之前留下的对应
beanName
的缓存
通过别名注册 BeanDefinition
@Override
public void registerAlias(String name, String alias) {
Assert.hasText(name, "'name' must not be empty");
Assert.hasText(alias, "'alias' must not be empty");
// 如果 beanName 与 alias 相同的话不记录 alias,并删除对应的 alias
if (alias.equals(name)) {
this.aliasMap.remove(alias);
}
else {
String registeredName = this.aliasMap.get(alias);
if (registeredName != null) {
if (registeredName.equals(name)) {
// An existing alias - no need to re-register
return;
}
// 如果 alias 不允许被覆盖则抛出异常
if (!allowAliasOverriding()) {
throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" +
name + "': It is already registered for name '" + registeredName + "'.");
}
}
// 当 A->B 存在时,若再次出现 A->C->B 时候则会抛出异常
checkForAliasCircle(name, alias);
this.aliasMap.put(alias, name);
}
}
alias
与beanName
相同情况处理。若alias
与beanName
并名称相同则不需要处理并删除掉原有alias
。alias
覆盖处理。若aliasName
已经使用并已经指向了另一beanName
则需要用户的设置进行处理。alias
循环检查。当 A->B 存在时,若再次出现 A->C->B 时候则会抛出异常。- 注册
alias
。
通知监听器解析及注册完成
// 扩展
alias 标签的解析
/**
* Process the given alias element, registering the alias with the registry.
*/
protected void processAliasRegistration(Element ele) {
// 获取 beanName
String name = ele.getAttribute(NAME_ATTRIBUTE);
// 获取 alias
String alias = ele.getAttribute(ALIAS_ATTRIBUTE);
boolean valid = true;
if (!StringUtils.hasText(name)) {
getReaderContext().error("Name must not be empty", ele);
valid = false;
}
if (!StringUtils.hasText(alias)) {
getReaderContext().error("Alias must not be empty", ele);
valid = false;
}
if (valid) {
try {
// 注册 alias
getReaderContext().getRegistry().registerAlias(name, alias);
}
catch (Exception ex) {
getReaderContext().error("Failed to register alias '" + alias +
"' for bean with name '" + name + "'", ele, ex);
}
// 别名注册后通知监听器做相应处理
getReaderContext().fireAliasRegistered(name, alias, extractSource(ele));
}
}
import 标签的解析
/**
* Parse an "import" element and load the bean definitions
* from the given resource into the bean factory.
*/
protected void importBeanDefinitionResource(Element ele) {
// 获取 resource 属性
String location = ele.getAttribute(RESOURCE_ATTRIBUTE);
// 如果不存在 resource 属性则不做任何处理
if (!StringUtils.hasText(location)) {
getReaderContext().error("Resource location must not be empty", ele);
return;
}
// Resolve system properties: e.g. "${user.dir}"
// 解析系统属性,格式如:"${user.dir}"
location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location);
Set<Resource> actualResources = new LinkedHashSet<Resource>(4);
// Discover whether the location is an absolute or relative URI
// 判定 location 是决定 URI 还是相对 RUI
boolean absoluteLocation = false;
try {
absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute();
}
catch (URISyntaxException ex) {
// cannot convert to an URI, considering the location relative
// unless it is the well-known Spring prefix "classpath*:"
}
// Absolute or relative?
if (absoluteLocation) {
try {
int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources);
if (logger.isDebugEnabled()) {
logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]");
}
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error(
"Failed to import bean definitions from URL location [" + location + "]", ele, ex);
}
}
else {
// No URL -> considering resource location as relative to the current file.
// 如果是相对地址则根据相对地址计算出绝对地址
try {
int importCount;
// Resource 存在多个子实现类,如 VfsResource,FileSystemResource 等,
// 而每个 resource 的 createRelative 方式实现都不一样,所以这里先使用子类的方法尝试解析
Resource relativeResource = getReaderContext().getResource().createRelative(location);
if (relativeResource.exists()) {
importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource);
actualResources.add(relativeResource);
}
else {
// 如果解析不成功,则使用默认的解析器 ResourcePatternResolver 进行解析
String baseLocation = getReaderContext().getResource().getURL().toString();
importCount = getReaderContext().getReader().loadBeanDefinitions(
StringUtils.applyRelativePath(baseLocation, location), actualResources);
}
if (logger.isDebugEnabled()) {
logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]");
}
}
catch (IOException ex) {
getReaderContext().error("Failed to resolve current resource location", ele, ex);
}
catch (BeanDefinitionStoreException ex) {
getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]",
ele, ex);
}
}
// 解析后进行监听器激活处理
Resource[] actResArray = actualResources.toArray(new Resource[actualResources.size()]);
getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele));
}
- 获取 resource 属性所表示的路径
- 解析路径中的系统属性,格式如 "${user.dir}"
- 判定 location 是绝对路径还是相对路径
- 如果是绝对路径则递归调用 bean 的解析过程,进行另一次的解析
- 如果是相对路径则计算出绝对路径并进行解析
- 通知监听器,解析完成
嵌入式 beans 标签的解析
递归调用解析 bean
/**
* Register each bean definition within the given root {@code <beans/>} element.
*/
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
postProcessXml(root);
this.delegate = parent;
}
参考书籍
《Spring源码深度解析》
MySQL优化概要
首先我们需要明确我们什么时候需要用到数据库:
- 当缓存并不能解决你的问题,比如写操作,事务操作
- 缓存的创建或过期需要通过数据库。
其次,我们可能需要一个专业的工具来指导我们优化:
mysqlreport
这是作为一个Mysql第三方的状态报告工具,其实就是将一下两行命令所获得的数据以一种更加人性化的方法呈现到我们眼前:
mysql> show status;
mysql> show engine innodb status;
你完全可以在自己电脑上先使用这个工具,这时候我们将逐条讲解工具为我们呈现的数据
s-db:~ # mysqlreport
MySQL 5.0.37-log uptime 1 15:11:47 Fri Apr 24 15:35:49 2009
__ Key ______________________________________________________________
Buffer used 100.37M of 2.00G %Used: 4.90
Current 1.02G %Usage: 51.07
Write hit 99.26%
Read hit 98.71%
__ Questions ________________________________________________________
Total 58.09M 411.7/s
DMS 30.62M 217.0/s %Total: 52.71
Com_ 21.23M 150.4/s 36.54
COM_QUIT 6.14M 43.5/s 10.58
+Unknown 104.71k 0.7/s 0.18
Slow 1 s 722 0.2/s 0.04 %DMS: 0.08 Log: ON
DMS 30.62M 217.0/s 52.71
SELECT 26.20M 185.7/s 45.10 85.56
UPDATE 3.72M 26.3/s 6.40 12.14
INSERT 649.28k 4.6/s 1.12 2.12
DELETE 54.13k 0.4/s 0.09 0.18
REPLACE 1.88k 0.0/s 0.00 0.01
Com_ 21.23M 150.4/s 36.54
change_db 21.18M 150.1/s 36.45
show_variab 8.94k 0.1/s 0.02
show_status 8.93k 0.1/s 0.02
__ SELECT and Sort ___________________________________________________
Scan 1.52M 10.8/s %SELECT: 5.80
Range 865.18k 6.1/s 3.30
Full join 3.18k 0.0/s 0.01
Range check 0 0/s 0.00
Full rng join 0 0/s 0.00
Sort scan 2.34M 16.6/s
Sort range 2.10M 14.9/s
Sort mrg pass 739 0.0/s
__ Table Locks ______________________________________________________
Waited 9.17k 0.1/s %Total: 0.03
Immediate 32.03M 227.0/s
__ Tables ___________________________________________________________
Open 512 of 512 %Cache: 100.00
Opened 2.78M 19.7/s
__ Connections ______________________________________________________
Max used 98 of 100 %Max: 98.00
Total 6.15M 43.6/s
__ Created Temp _____________________________________________________
Disk table 332.75k 2.4/s
Table 2.25M 16.0/s Size: 32.0M
File 1.48k 0.0/s
__ Threads __________________________________________________________
Running 2 of 5
Cached 0 of 0 %Hit: 0
Created 6.15M 43.6/s
Slow 0 0/s
__ Aborted __________________________________________________________
Clients 5.70k 0.0/s
Connects 36 0.0/s
__ Bytes ____________________________________________________________
Sent 2.74G 19.4k/s
Received 3.70G 26.2k/s
__ InnoDB Buffer Pool ________________________________________________
Usage 1.00G of 1.00G %Used: 100.00
Read hit 99.84%
Pages
Free 1 %Total: 0.00
Data 65.16k 99.43 %Drty: 4.00
Misc 374 0.57
Latched 0 0.00
Reads 78.05M 553.2/s
From file 125.51k 0.9/s 0.16
Ahead Rnd 1740 0.0/s
Ahead Sql 7 0.0/s
Writes 16.06M 113.8/s
Flushes 1.30M 9.2/s
Wait Free 0 0/s
__ InnoDB Lock ______________________________________________________
Waits 1441 0.0/s
Current 0
Time acquiring
Total 1178 ms
Average 0 ms
Max 39 ms
__ InnoDB Data, Pages, Rows __________________________________________
Data
Reads 142.84k 1.0/s
Writes 1.26M 8.9/s
fsync 191.53k 1.4/s
Pending
Reads 0
Writes 0
fsync 0
Pages
Created 1.05k 0.0/s
Read 173.91k 1.2/s
Written 1.30M 9.2/s
Rows
Deleted 0 0/s
Inserted 149.59k 1.1/s
Read 3.72G 26.4k/s
Updated 2.51M 17.8/s
首先最上面的uptime
中我们可以看到MySQL数据库的运行时间,而报告中的各项数据统计,都是通过这段运行时间的累计数据计算而得,所以,我们希望这段时间尽可能地长,至少覆盖站点高负载的时段,这样我们采集的数据才具有较好的代表性。
索引的优化说明
对于索引的介绍这里就不做过多的重复了。
我们需要知道的是数据库中大概分为两种扫描模式:
- 全表扫描
- 索引扫描
大多数时候索引扫描会比全表扫描有着更好的性能,但是并非绝对,当我们需要一个表中绝大多数数据(这个数量可能是60%以上)的时候,全表扫描就可能获得比索引扫描更好的性能,相信这并不难理解,毕竟我们阅读一本书的时候,如果向获取书中绝大部分的知识可能一页一页阅读更加迅速。
当然,或许有时候数据库的查询优化器会帮助我们鉴别什么时候我们需要使用这些索引,但是我认为这应该是当我们创建一个表的时候需要考虑的内容。
索引有挺多种的,除了普通索引,还有唯一索引,主键,全文索引等,但是,它们只要是索引,都会具备索引扫描的功能,不同的是它们可能会满足我们一些额外的需求。
需要特别强调的是:构建索引,构建什么索引,在哪个键构建索引,这是我们自己的事,没有什么工具能够帮助我们完成这些本是我们的工作。
一般来说,如果一个字段出现在查询语句中基于行的选择、过滤或排序条件中,那么为该字段建立索引便是有价值的,但这也不是绝对的。我们很难给出一个描述了所有情况的列表供你参考,因为查询的过程非常复杂,它可能包含多列索引、组合索引以及联合查询等情况,所以,关键在于掌握分析方法,这样你便能够应付任何的困境。
这时候有一个分析方法是我们必须掌握的:explain
。它只复杂查询语句的分析。
我们或许可以看看实例:
CREATE TABLE `test` (
`id` int(11) NOT NULL auto_increment,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`))
然后我填充了一些记录,内容是什么并不重要,我们这里只是用explain来分析查询语句是否使用了索引。
让我们先使用主键试试
mysql> explain select * from test where id=1;
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | test | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
这里我们可以看到:
type
为const
,意味着这次查询通过索引直接找到一个匹配行,所以优化器认为它的时间复杂度为常量。key
为PRIMARY
,意味着这次查询使用了主键索引。
现在让我们换成普通属性:
mysql> explain select * from test where name='colin';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | test | ALL | NULL | NULL | NULL | NULL | 4 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
这时候的type
就变成了ALL
,表示全表扫描了。
也许我们加上索引情况会有所不同吧?
mysql> alter table test add key name(name);
mysql> explain select * from test where name='colin';
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
| 1 | SIMPLE | test | ref | name | name | 257 | const | 1 | Using where |
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
意料之中~
也许……我们可以试试模糊查询:
mysql> explain select * from test where name like '%colin';
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | test | ALL | NULL | NULL | NULL | NULL | 4 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
看来此时的索引是帮不上忙了(少用模糊查询)
对于包含group by
的查询,数据库一般需要先将记录分组后放置在新的临时表中,然后分别对它们进行函数计算,比如count()
、sum()
或max()
等。当有恰当的索引存在时,group by
有时也可以使用索引来取代创建临时表,这当然是我们所希望的。以下这个SQL语句便利用了normal_key
索引,避免了创建临时表。
mysql> explain select count(id) from key_t where key1=777 group by key3;
+----+-------------+-------+------+---------------+------------+---------+-------+------+-----------------------------------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------------+---------+-------+------+-----------------------------------------------------------+
| 1 | SIMPLE | key_t | ref | normal_key | normal_key | 4 | const | 2154 | Using where; Using index; Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------------+---------+-------+------+-----------------------------------------------------------+
先等等,我们还有一个重点
组合索引
(就如同书目录的章和节)
最左前缀:
顾名思义,最左优先,当我们创建了一个(key1, key2, key3...)
的索引的时候会按照如下的索引,key1
,(key1, key2)
,(key1, key2, key3)
...
这时候就会有个坑了……
先让我们来看看这条查询:
mysql> select * from key_t where key2=777 limit 10;
+--------+------+------+------+
| id | key1 | key2 | key3 |
+--------+------+------+------+
| 327233 | 643 | 777 | 781 |
| 686994 | 765 | 777 | 781 |
| 159907 | 766 | 777 | 782 |
| 61518 | 769 | 777 | 780 |
| 274629 | 769 | 777 | 780 |
| 633439 | 769 | 777 | 780 |
| 774191 | 769 | 777 | 780 |
| 109562 | 769 | 777 | 781 |
| 130013 | 769 | 777 | 781 |
| 139458 | 769 | 777 | 781 |
+--------+------+------+------+
10 rows in set (0.38 sec)
380ms!没错,可能你会想,我们并没有使用索引,它是全表查询,但是……:
mysql> explain select * from key_t where key2=777 limit 10;
+----+-------------+-------+-------+---------------+------------+---------+------+---------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------------+---------+------+---------+--------------------------+
| 1 | SIMPLE | key_t | index | NULL | normal_key | 12 | NULL | 1000417 | Using where; Using index |
+----+-------------+-------+-------+---------------+------------+---------+------+---------+--------------------------+
对不起,它用了normal_key
,仔细看上面十行数据似乎都是按照key1
字段来排序的,事实也是如此,这说明查询是依据normal_key
来扫描而不是数据本身扫描,在Innodb
类型表中数据的存储顺序是按照主键来排序的。
下面让我们将这个表转换为MyISAM类型来看看:
mysql> alter table key_t type=myisam;
Query OK, 1000000 rows affected, 1 warning (33.49 sec)Records: 1000000 Duplicates: 0 Warnings: 0
然后查询:
mysql> select * from key_t_myisam where key2=777 limit 10;
+------+------+------+------+
| id | key1 | key2 | key3 |
+------+------+------+------+
| 1035 | 771 | 777 | 781 |
| 3175 | 771 | 777 | 781 |
| 4126 | 771 | 777 | 781 |
| 5443 | 770 | 777 | 780 |
| 6066 | 771 | 777 | 781 |
| 6267 | 770 | 777 | 780 |
| 6317 | 770 | 777 | 780 |
| 6496 | 771 | 777 | 781 |
| 8262 | 770 | 777 | 780 |
| 9083 | 771 | 777 | 780 |
+------+------+------+------+
10 rows in set (0.00 sec)
看看分析结果:
mysql> explain select * from key_t_myisam where key2=777 limit 10;
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| 1 | SIMPLE | key_t | ALL | NULL | NULL | NULL | NULL | 1000000 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
这才是货真价实的全表扫描啊!
我们足可以想象当组合索引不能直接发挥作用的时候,反而给查询带来了巨大的负担,一个包含多个字段的组合索引的尺寸可能已经超过了数据本身。
如果你希望结果仍然按照key1
排序,这不是什么问题,你可以增加一个包含(key2,key1)
字段的组合索引,注意它们的顺序,(key1,key2)
索引和(key2,key1)
索引完全不同,你必须根据需要来进行抉择,而优化器对此无能为力。
以上我们只是举了个简单的例子,并不是鼓励你用全表扫描取代索引扫描,而是希望借此强调,一定要根据查询的需要来设计有针对性的组合索引,因为在实际应用中,很多查询不像例子中的那么简单,一个量身定制的组合索引肯定要比全表扫描更加高效,这是毋庸置疑的。
慢查询工具分析
我们很容易对所有查询语句进行分析,并建立适当的索引,但这只是纸上谈兵,当到了运行环境后,随着实际数据的积累,查询计算的开销也逐渐增加,也许你会发现有些索引设计得并不理想,不可否认,错误是在所难免的。可是,在运行环境上对各种查询进行explain
分析显然很不现实,更难的是你不知道何时去分析哪些查询。
最好的办法是对每一条查询语句的时间进行记录然后再对其中比较慢的查询去分析,通过Web应用的日志功能可能也是一个比较不错的主意。
但是我们这里是介绍数据库,数据库中还有一种方法可以帮助我们达到我们所想要的目的,它提供了慢查询日志,可以将执行时间超过预设值的所有查询记录到日志中,以供后期分析。
在Mysql中的my.cnf开启慢查询工具
long_query_time = 1
log-slow-queries = /data/var/mysql_slow.log
这意味着MySQL会自动将执行时间超过1秒的查询记录在指定路径的mysql_slow.log文件中。除此之外,你还可以将所有没有使用索引的查询也记录下来,只需增加以下选项即可:
log-queries-not-using-indexes
而在之前提及的mysqlreport
中,我们也可以看到有关慢查询的统计:
Slow 1 s 722 0.2/s 0.04 %DMS: 0.08 Log: ON
我们可以使用第三方工具mysqlsla,它有着比之其他工具更加清晰的统计风格。
我们还有一个优化策略,使用缓存,缓存索引,这样使用到索引的时候就不需要在磁盘中读取数据了。
在mysqlreport中也有这方面的信息:
__ Key ______________________________________________________________
Buffer used 100.37M of 2.00G %Used: 4.90
Current 1.02G %Usage: 51.07
Write hit 99.26%
Read hit 98.71%
这里的缓存和之前介绍的其他缓存在本质上没什么两样,Write hit和Read hit分别代表了写缓存的命中率和读缓存的命中率,这里都在98%以上,比较正常。
当然最后在索引这儿提一下使用它的代价(我们大多数优化策略其实都需要付出一些代价的)
首先是空间,索引使用空间比之普通文件较大,但是在如今存储空间不值钱的年代这完全不是事。
其次,当建立索引的字段发生更新时,会引发索引本身的更新,这将产生不小的计算开销。
最后,索引需要我们花费一些额外的时间来维护。
锁
锁机制是保证当有多个用户并发访问数据库某个资源的时候,并发访问的一致性。
我们可以认为查询的时间开销主要包括两部分,即
- 查询本身的计算时间
- 查询开始前的等待时间
所以说索引影响的是前者,而锁机制影响的是后者。显然,我们的目标很明确,那就是减少等待时间。
在mysqlreport中也有相应的记录:
__ Table Locks ______________________________________________________
Waited 9.17k 0.1/s %Total: 0.03
Immediate 32.03M 227.0/s
Waited
表示有多少次查询需要等待表锁定;Immediate
表示有多少次查询可以立即获得表锁定,同时后面还有一个比例,表示等待表锁定的查询次数占所有查询次数的百分比,这里是0.03%,非常好,但为什么这么低呢?这需要了解MyISAM的表锁定机制。
MyISAM的表锁定可以允许多个线程同时读取数据,比如select
查询,它们之间是不需要锁等待的。但是对于更新操作(如update
操作),它会排斥对当前表的所有其他查询,包括select
查询。除此之外,更新操作有着默认的高优先级,这意味着当表锁释放后,更新操作将先获得锁定,然后才轮到读取操作。也就是说,如果有很多update
操作排着长队,那么对当前表的select
查询必须等到所有的更新都完成之后才能开始。
如果你的站点主要依靠用户创造内容,那么频繁的数据更新在所难免,它将会给select
等读取操作带来很大的影响,你可能会需要innodb的行锁定来为你提高一定的性能。
但是我们需要认清楚,行锁定只是能够提高你在update
上的等待时间,但是却不能加速update
的时间,当你的update
太过密集的时候,你的磁盘读写速度会称为限制你的性能的一个门槛,这种情形下或许行锁定的消耗就决定它并不是最好的选择了。
事务
事务可以说是吸引大家选择innodb的一个主要原因,但是,在选择它的时候你需要先问问自己,你是否真的这么需要事务,如果没有事务,你有什么不能解决的问题,毕竟事务对于性能也是一大消耗。
Innodb的实现方式是预写日志方式(WAL),当有事务提交时,也就是只有当事务日志写入磁盘后才更新数据和索引,这样即使数据库崩溃,也可以通过事务日志来恢复数据和索引。Innodb首先将它写到内存中的事务日志缓冲区,随后当事务日志写入磁盘时,Innodb才更新实际数据和索引。这里有一个关键点,那就是事务日志何时写入磁盘。
为此,MySQL提供了一个配置选项,它有三个可选的值:
innodb_flush_log_at_trx_commit = 1
表示事务提交时立即将事务日志写入磁盘,同时数据和索引也立即更新。这符合事务的持久性原则。innodb_flush_log_at_trx_commit = 0
表示事务提交时不立即将事务日志写入磁盘,而是每隔1秒写入磁盘文件一次,并且刷新到磁盘,同时更新数据和索引。这样一来,如果mysqld崩溃,那么在内存中事务日志缓冲区最近1秒的数据将会丢失,这些更新将永远无法恢复。innodb_flush_log_at_trx_commit = 2
表示事务提交时立即写入磁盘文件,但是不立即刷新到磁盘,而是每隔1秒刷新到磁盘一次,同时更新数据和索引。在这种情况下,即使mysqld崩溃后,位于内核缓冲区的事务日志仍然不会丢失,只有当操作系统崩溃的时候才会丢失最后1秒的数据。
显然,将innodb_flush_log_at_trx_commit
设置为0可以获得最佳性能,同时它的数据丢失可能性也最大。
另一个重要的配置选项是Innodb数据和索引的内存缓冲池大小,MySQL提供了innodb_buffer_pool_size
选项来设置这个数值,如果你在MySQL中大量使用Innodb类型表,则可以将缓冲池大小设置为物理内存的80%,并持续关注它的使用率,这时候mysqlreport又提供了方便。
__ InnoDB Buffer Pool ________________________________________________
Usage 1.00G of 1.00G %Used: 100.00
Read hit 99.84%
使用查询缓存
查询缓存的目的很简单,将select
查询的结果缓存在内存中,以供下次直接获取。在默认情况下,MySQL是没有开启查询缓存的,我们可以进行以下配置:
query_cache_size = 268435456
query_cache_type = 1
query_cache_limit = 1048576
这样一来,MySQL将拥有256MB的内存空间来缓存查询结果。对于以select查询为主的应用,查询缓存理所当然地起到性能提升的作用,不论是Innodb还是MyISAM,查询缓存都可以很好地工作,因为它在逻辑中位于比较高的层次。
但是,查询缓存有一个需要注意的问题,那就是缓存过期策略,MySQL采用的机制是,当一个数据表有更新操作(比如update
或者insert
)后,那么涉及这个表的所有查询缓存都会失效。这的确令人比较沮丧,但是MySQL这样做是不希望引入新的开销而自找麻烦,所以“宁可错杀一千,不可放过一个”。这样一来,对于select
和update
混合的应用来说,查询缓存反而可能会添乱
关于mysqlreport中查询缓存的报告:
__ Query Cache ______________________________________________________
Memory usage 38.05M of 256.00M %Used: 14.86
Block Fragmnt 4.29%
Hits 12.74k 33.3/s
Inserts 58.21k 152.4/s
Insrt:Prune 58.21k:1 152.4/s
Hit:Insert 0.22:1
如果你的应用中对于密集select
的数据表很少更新,很适合于使用查询缓存。
临时表
我们或许看到一些explain
查询在分析时出现Using temporary的状态,这意味着查询过程中需要创建临时表来存储中间数据,我们需要通过合理的索引来避免它。另一方面,当临时表在所难免时,我们也要尽量减少临时表本身的开销,通过mysqlreport报告中的Created Temp部分,我们可以看到:
_ Created Temp _____________________________________________________
Disk table 864.89k 2.0/s
Table 7.06M 16.1/s Size: 32.0M
File 9.22k 0.0/s
MySQL可以将临时表创建在磁盘(Disk table)、内存(Table)以及临时文件(File)中,显然,在磁盘上创建临时表的开销最大,所以我们希望MySQL尽量不要在磁盘上创建临时表。
在MySQL的配置中,我们可以通过tmp_table_size
选项来设置用于存储临时表的内存空间大小,一旦这个空间不够用,MySQL将会启用磁盘来保存临时表,你可以根据mysqlreport的统计尽量给临时表设置较大的内存空间。
线程池
我们知道,MySQL采用多线程来处理并发的连接,通过mysqlreport中的Threads部分,我们可以看到线程创建的统计结果:
Threads _____________________________________________________
Running 2 of 5
Cached 0 of 0 %Hit: 0
Created 6.15M 43.6/s
Slow 0 0/s
也许你会觉得创建线程的消耗不值一提,但是我们所谓优化都是在你系统繁忙下的救命稻草。
一个比较好的办法是在应用中尽量使用持久连接,这将在一定程度上减少线程的重复创建。另一方面,从上面的Cached=0可以看出,这些线程并没有被复用,我们可以在my.cnf中设置以下选项:
thread_cache_size = 100
抛弃关系型数据库
我们应该知道,在关系型数据库里指导我们设计数据库表的时候可以根据范式来设计,我们一般会选择第三范式,简单地说,第三范式要求在一个数据表中,非主键字段之间不能存在依赖关系,这样一来,它可以避免更新异常、插入异常和删除异常,保证关系的一致性,并且减少数据冗余。
其实我们并不需要去刻意设计,只要你设计表的时候思维正常,大多数时候都是第三范式。
但是对于某些应用这样做可能会极大降低我们的性能
比如对于这样两张表
(用户ID,好友ID,好友昵称)
(用户ID,用户昵称,用户邮箱,注册时间,联系电话)
它们肯定不符合第三范式,因为好友昵称依赖与好友ID,但是这两个很容易是同时取出,而修改好友昵称的机会少得可怜,甚至你的应用可能根本不想支持,这时候抛弃范式来设计就会有着更好的效果。
还有一个比较经典的例子:
社交网站中的好友Feed功能,你一定非常熟悉,当你的好友或者关注的人发生一些事件的时候,你会在自己的空间看到这些动态,看起来很简单的功能,你将如何实现呢?如果在你每次刷新空间的时候都要对每个好友的最新事件进行一番查询,甚至使用可怕而昂贵的join联合查询,当你有500个好友的时候,开销可想而知。这个时候,出于性能的考虑,可以使用反范式化设计,将这些动态为所有关注它的用户保存一份副本,当然,这会带来大量的写开销,但是这些写操作完全可以异步进行,而不影响用户的体验。
这时候如果使用NOSQL来存储这些冗余的副本可能会给你带来意想不到的性能的提升。
下面是一些启发性优化策略:
Mysql性能优化20+条经验
最后参考资料:
《构建高性能Web站点》,推荐必读书
初学Kafka——Kafka配置
系统环境
我使用的是vagrant配置的Debian8环境
$ ~ uname -a
Linux debian-jessie 3.16.0-4-amd64 #1 SMP Debian 3.16.7-ckt20-1+deb8u4 (2016-02-29) x86_64 GNU/Linux
$ ~ free -m
total used free shared buffers cached
Mem: 494 155 338 1 10 88
-/+ buffers/cache: 57 437
Swap: 461 4 457
$ ~ df -h
文件系统 容量 已用 可用 已用% 挂载点
/dev/sda1 9.2G 2.7G 6.1G 30% /
udev 10M 0 10M 0% /dev
tmpfs 99M 4.4M 95M 5% /run
tmpfs 248M 0 248M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 248M 0 248M 0% /sys/fs/cgroup
tmpfs 50M 0 50M 0% /run/user/1000
JDK以及Kafka环境配置
91 # Java环境配置
92 export JAVA_HOME="$HOME/.utils/jdk"
93 export PATH="$JAVA_HOME/bin:$PATH"
94 export CLASSPATH="$JAVA_HOME/lib:$PATH"
95
96 # Kafka环境配置
97 export KAFKA_HOME="$HOME/.utils/kafka"
98 export PATH="$KAFKA_HOME/bin:$PATH"
99 export KAFKA_HEAP_OPTS="-Xmx256M -Xms128M" #与下面错误有关
这里可以看出,我将Java和kafka放入/home/.utils
中
这里我们需要先看一下kafka
的目录
$ kafka ls
bin config libs LICENSE logs NOTICE site-docs
进入bin
$ bin ls
connect-distributed.sh kafka-consumer-groups.sh kafka-reassign-partitions.sh kafka-simple-consumer-shell.sh zookeeper-server-start.sh
connect-standalone.sh kafka-consumer-offset-checker.sh kafka-replay-log-producer.sh kafka-topics.sh zookeeper-server-stop.sh
kafka-acls.sh kafka-consumer-perf-test.sh kafka-replica-verification.sh kafka-verifiable-consumer.sh zookeeper-shell.sh
kafka-configs.sh kafka-mirror-maker.sh kafka-run-class.sh kafka-verifiable-producer.sh
kafka-console-consumer.sh kafka-preferred-replica-election.sh kafka-server-start.sh windows
kafka-console-producer.sh kafka-producer-perf-test.sh kafka-server-stop.sh zookeeper-security-migration.sh
进入config
$ kafka cd config
$ config ls
connect-console-sink.properties connect-file-sink.properties connect-standalone.properties producer.properties tools-log4j.properties
connect-console-source.properties connect-file-source.properties consumer.properties server.properties zookeeper.properties
connect-distributed.properties connect-log4j.properties log4j.properties test-log4j.properties
启动
启动zookeeper
这里我们使用kafka
自带的zookeeper
从上面的bin
中我们可以找到zookeeper-server-start.sh
用来启动zookeeper
,配置使用config
中的zookeeper.properties
进入zookeeper.properties
可以看到
15 # the directory where the snapshot is stored.
16 dataDir=/data/kafka/zookeeper
17 # the port at which the clients will connect
18 clientPort=2181
19 # disable the per-ip limit on the number of connections since this is a non-production config
20 maxClientCnxns=0
我们将数据放入/data/kafka/zookeeper
启动命令
zookeeper-server-start.sh ~/.utils/kafka/config/zookeeper.properties
启动kafka
kafka
的配置文件我们同样选择config
中的server.properties
其中我们只需要关注这几个配置
broker.id = 0
port = 9092
log.dirs = /data/kafka-logs # 非log,数据存储处
zookeeper.connect = localhost:2181
如果是不同副本前三个需要不一致
启动命令
kafka-server-start.sh ~/.utils/kafka/config/server.properties
这里我遇到一个错误:There is insufficient memory for the Java Runtime Environment to continue
其具体描述与解决方法如下:
解决办法
可以通过jps -l
查看运行在jdk上的应用程序
$ ~ jps -l
6049 sun.tools.jps.Jps
5714 kafka.Kafka
5561 org.apache.zookeeper.server.quorum.QuorumPeerMain
可看到zookeeper
与kafka
成功运行
创建Topic
kafka-topics.sh --create --zookeeper localhost:2181 --topic topic_name --partition 3 --replication-factor 1
--partition
指定分区数
--replication-factor
指定副本数
通过下面命令来查看创建的topic
$ ~ kafka-topics.sh --list --zookeeper localhost:2181
test1
这样kafka
就算搭建起来了
Tornado源码阅读之后
大概是上个星期将Tornado的源码大概读了一遍,有这么几篇文章值得参考:
主要仔细读的就tcpsever.py
, tcpclient.py
, httpclient.py
, httpserver.py
, web.py
, ioloop.py
。
所谓Tornado的高并发其实是epoll
/kqueue
的封装,对读写进行事件的封装。在全局仅有一个的IOLoop的实例中进行死循环来等待请求发送,这其实主要是避免了IO(网络连接)的等待,而我们在等到请求之后会执行add_handler
方法,这其中其实就是我们在Application
中所传入的类,这里面类使用了__call__
进行包装,从而可以达到调用其类本身就可以调用其中的RequestHandler
的对应方法_execute()
, 这里面就是处理了我们的请求,并根据请求的方法分配到我们实现的子类自己所实现的get
, post
方法等,其中检查xsrf
等也是在这里面开始进行。
其实这次阅读还是有一些没有深入的地方,例如iostream.py
中对于数据流的处理,例如各种util也没有详细阅读。
下次计划准备阅读Flask
及其相应的代码,毕竟在Python框架里面,我对这两个框架是最为熟悉的了,也可以与现在阅读的Tornado做一个横向对比。
csrf(xsrf)原理与应对策略
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
进攻方式
攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过(携带Session)的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了web中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
防御措施
-
验证码
-
检查Referer
一般向
www.xxx.com
发送请求的referer也是www.xxx.com
,而如果是CSRF攻击传来的请求,Referer字段会是包含恶意网址的地址,不会位于www.xxx.com
之下,这时候服务器就能识别出恶意的访问。 -
Token
由于CSRF的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在cookie中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行CSRF攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过CSRF传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验token的值为空或者错误,拒绝这个可疑请求。
Tornado中的做法
在Application的settings中添加
"cookie_secret": "xxxxxxxxxxxxx"`
"xsrf_cookies": True
就会自动在非get, head, option
请求中检查_xsrf
参数是否存在且是否正确
而这个参数是通过以下函数产生的
def _get_raw_xsrf_token(self):
"""Read or generate the xsrf token in its raw form.
"""
if not hasattr(self, '_raw_xsrf_token'):
cookie = self.get_cookie("_xsrf")
if cookie:
version, token, timestamp = self._decode_xsrf_token(cookie)
else:
version, token, timestamp = None, None, None
if token is None:
version = None
token = os.urandom(16)
timestamp = time.time()
self._raw_xsrf_token = (version, token, timestamp)
return self._raw_xsrf_token
然后在finish
的时候回触发check_xsrf_cookie
def check_xsrf_cookie(self):
token = (self.get_argument("_xsrf", None) or
self.request.headers.get("X-Xsrftoken") or
self.request.headers.get("X-Csrftoken"))
if not token:
raise HTTPError(403, "'_xsrf' argument missing from POST")
_, token, _ = self._decode_xsrf_token(token)
_, expected_token, _ = self._get_raw_xsrf_token()
if not _time_independent_equals(utf8(token), utf8(expected_token)):
raise HTTPError(403, "XSRF cookie does not match POST argument")
这有什么用呢,之前我一直想写一个单元测试,这也是为了写单元测试,发送请求时候绕开xsrf时候所写的,通过伪造一个相同的函数就可以绕开了。
Java 技巧—懒加载
package io.github.binglau;
import java.util.function.Supplier;
/**
* 文件描述: 懒加载大对象 Heavy
* 利用类加载机制达到 LazyLoad 为单例
*/
public final class LazyLoad {
private LazyLoad() {
}
private static class Holder {
private static final LazyLoad INSTANCE = new LazyLoad();
}
public static final LazyLoad getInstance() {
return Holder.INSTANCE;
}
private Supplier<Heavy> heavy = this::createAndCacheHeavy;
private Normal normal = new Normal();
public Heavy getHeavy() {
return heavy.get();
}
public Normal getNormal() {
return normal;
}
private synchronized Heavy createAndCacheHeavy() {
class HeavyFactory implements Supplier<Heavy> {
private final Heavy heavyInstance = new Heavy();
@Override
public Heavy get() {
return heavyInstance;
}
}
if (!HeavyFactory.class.isInstance(heavy)) {
heavy = new HeavyFactory();
}
return heavy.get();
}
public static void main(String[] args) throws InterruptedException {
LazyLoad.getInstance().getNormal();
LazyLoad.getInstance().getHeavy();
}
}
class Heavy {
public Heavy() {
System.out.println("Creating Heavy ...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("... Heavy created");
}
}
class Normal {
public Normal() {
System.out.println("Creating Normal ...");
System.out.println("... Normal created");
}
}
AOP的道理
代理模式
动机
在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起到 中介的作用,并且可以通过代理对象去掉客户不能看到的内容和服务或者添加客户需要的额外服务。
通过引入一个新的对象(如小图片和远程代理对象)来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
UML
代码实例
package io.github.binglau.proxy;
/**
* 类ProxySubject.java的实现描述:TODO:类实现描述
*
* @author bingjian.lbj 2016-09-17 下午6:42
*/
public interface ProxySubject {
void operation();
}
package io.github.binglau.proxy;
/**
* 类RealSubject.java的实现描述:TODO:类实现描述
*
* @author bingjian.lbj 2016-09-17 下午6:42
*/
public class RealSubject implements ProxySubject {
private String requestContent;
public RealSubject(String requestContent) {
this.requestContent = requestContent;
}
@Override
public void operation() {
System.out.println("RealSubject request");
}
}
package io.github.binglau.proxy;
/**
* 1. 保存一个引用使得代理可以访问实体。若RealSubject和ProxySubject的接口相同, Proxy会引用ProxySubject。
* 2. 提供一个与ProxySubject的接口相同的接口,这样代理就可以用来替代实体。
* 3. 控制对实体的存取,并可能负责创建和删除它。
* 4. 其他功能依赖于代理的类型:
* 1) Remote Proxy负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。
* 2) Virtual Proxy可以缓存实体的附加信息,以便延迟对它的访问。
* 3) Protection Proxy检查调用者是否具有实现一个请求所必需的访问权限。
* @author bingjian.lbj 2016-09-17 下午6:42
*/
public class Proxy implements ProxySubject{
private String requestContent;
private RealSubject subject;
public Proxy(String requestContent) {
this.requestContent = requestContent;
}
@Override
public void operation() {
if (subject == null) {
subject = new RealSubject(requestContent);
}
System.out.println("Proxy request");
subject.operation();
}
public static void main(String[] args) {
ProxySubject subject = new Proxy("test");
subject.request();
}
}
使用环境
- 远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在另一台主机中,远程代理又叫做大使(Ambassador)。
- 虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
- Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。
- 保护(Protect or Access)代理:控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
- 缓冲(Cache)代理:为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。
- 防火墙(Firewall)代理:保护目标不让恶意用户接近。
- 同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突。
- 智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。
Java 中的动态代理
package io.github.binglau.proxy.static_proxy;
public interface Hello { // 1. 公共接口
void say(String name);
}
package io.github.binglau.proxy.static_proxy;
public class HelloImpl implements Hello { // 1. 委托类
@Override
public void say(String name) {
System.out.println("Hello! " + name);
}
}
package io.github.binglau.proxy.dynamic;
import io.github.binglau.proxy.static_proxy.Hello;
import io.github.binglau.proxy.static_proxy.HelloImpl;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 主要是解决静态代理中对于不同的接口都需要实现一个代理类的情况
public class DynamicProxy implements InvocationHandler { // 2
private Object target;
public DynamicProxy(Object target) {
this.target = target;
}
/**
在代理对象调用任何一个方法时都会调用的,方法不同会导致第二个参数method不同,第一个参数是代理对象(表示哪个代理对象调用了method方法),第二个参数是 Method 对象(表示哪个方法被调用了),第三个参数是指定调用方法的参数。
**/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object result = method.invoke(target, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
@SuppressWarnings("unchecked")
public <T> T getProxy() {
/*
* params:
* 1. ClassLoader
* 2. 该实现类的所有接口
* 3. 动态代理对象
*/
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this
);
}
public static void main(String[] args) {
Hello hello = new HelloImpl();
DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy = dynamicProxy.getProxy(); // 3
helloProxy.say("Jack");
System.out.println(Proxy.getInvocationHandler(helloProxy)); // 获得代理对象对应的调用处理器对象
/**
Proxy 其他方法:
Class getProxyClass(ClassLoader loader, Class[] interfaces): 根据类加载器和实现的接口获得代理类。
**/
}
}
/**
Before
Hello! Jack
After
**/
Java实现动态代理的大致步骤如下:
-
定义一个委托类和公共接口。
-
自己定义一个类(调用处理器类,即实现
InvocationHandler
接口),这个类的目的是指定运行时将生成的代理类需要完成的具体任务(包括Preprocess和Postprocess),即代理类调用任何方法都会经过这个调用处理器类。 -
生成代理对象(当然也会生成代理类),需要为他指定:
- 委托对象
- 实现的一系列接口(3)调用处理器类的实例。
因此可以看出一个代理对象对应一个委托对象,对应一个调用处理器实例。
CGLib 动态代理
package io.github.binglau.proxy.cglib;
import io.github.binglau.proxy.static_proxy.Hello;
import io.github.binglau.proxy.static_proxy.HelloImpl;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 为了代理没有接口的类
public class CGLibProxy implements MethodInterceptor {
public <T> T getProxy(Class<T> cls) {
// Enhancer是CGLib的字节码增强器,可以方便的对类进行扩展,内部调用GeneratorStrategy.generate方法生成代理类的字节码
return (T) Enhancer.create(cls, this);
}
// 同 JDK 动态代理一样,每个方法的调用都会调用 intercept 方法
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
before();
Object result = proxy.invokeSuper(obj, args);
after();
return result;
}
private void before() {
System.out.println("Before");
}
private void after() {
System.out.println("After");
}
public static void main(String[] args) {
CGLibProxy cgLibProxy = new CGLibProxy();
Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class);
helloProxy.say("Jack");
}
}
/**
Before
Hello! Jack
After
**/
Spring 的 AOP
AOP 框架不会去修改字节码,而是在内存中临时为方法生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理。具体可看上文介绍。
如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLib 来动态代理目标类。CGLib(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLib 是通过继承的方式做的动态代理,因此如果某个类被标记为 final
,那么它是无法使用 CGLib 做动态代理的。
参考
《从零开始写 Java Web 框架》
MySQL-EXPLAIN 注
EXPLAIN 中列的含义
- id: 标识
SELECT
所属的行。如果在语句中没有子查询或联合,那么只会有唯一的SELECT
,于是每一行在这个列中都将显示一个 1。否则,内层SELECT
语句一般会顺序编号,对应于其在原始语句中的位置。 - select_type: 显示了对应行是简单还是复杂
SELECT
(如果是后者,那么是三种复杂类型中的哪一种)。SIMPLE
:查询中不包含子查询和UNION
PRIMARY
:有任何复杂的子部分SUBQUERY
:包含在SELECT
列表中的子查询的SELECT
(换句话说,不在FROM
子句中)DERIVED
:表示包含在FROM
子句的子查询中的SELECT
,MySQL 会递归执行并将结果放在一个临时表中。服务器内部称为『派生表』,因为该临时表是从子查询中派生来的。UNION
:在UNION
中的第二个和随后的SELECT
被标记为UNION
。第一个SELECT
被标记就好像它以部分外查询来执行。UNION RESULT
:用来从 UNION 的匿名临时表检索结果的SELECT
被标记为UNION RESULT
。
- table: 显示对应行正在访问哪个表。当 FROM 子句中有子查询或有
UNION
时,由于 MySQL 创建的匿名临时表仅在查询执行过程中存在。- 当在
FROM
子句中有子查询时,table 列是<derivedN>
的形式,其中 N 是子查询的 id、这总是『向前引用』—— 换言之,N 指向EXPLAIN
输出中后面的一行。 - 当有
UNION
时,UNION RESULT
的 table 列包含一个参与UNION
的 id 列表。这总是『向后引用』,因为UNION RESULT
出现在UNION
中所有参与行之后。
- 当在
- partitions: 代表给定表所使用的分区。
- type: 访问类型——换言之就是 MySQL 决定如何查找表中的行。下面是最重要的访问方法,依次从最差到最优:
- ALL:全表扫描,通常意味着 MySQL 必须扫描整张表,从头到尾,去找到需要的行。(例外是使用了 LIMIT 或者在 Extra 列中显示 "Using distinct/not exists")
- index: 这个跟全表扫描一样,只是 MySQL 扫描表时按索引次序进行而不是行。它的主要优点是避免了排序;最大的缺点是要承担按索引次序读取整个表的开销。这通常意味着若是按随机次序访问行,开销将会非常大。如果在 Extra 列中看到 "Using index",说明 MySQL 正在使用覆盖索引,它只扫描索引的数据,而不是按索引次序的每一行。它比按索引次序全表扫描的开销要少很多。
- range: 范围扫描就是一个有限制的索引扫描,它开始于索引里的某一点,返回匹配这个值域的行。它比全索引扫描好一些,因为它用不着遍历全部索引。显而易见的范围扫描是带有
BETWEEN
或在WHERE
子句中带有 > 的查询。当 MySQL 使用索引去查找一系列值时,例如IN()
和OR
列表,也会显示为范围扫描。然而,这两者其实是相当不同的访问类型,在性能上有重要的差异。具体可查看 《高性能 MySQL》 第五章文章『什么是范围条件』。 - ref:这是一种索引访问(有时也叫做索引查找),它返回所有匹配某个单个值的行。然而,它有可能会找到多个符合条件的行,因此,它是查找和扫描的混合体。此类索引访问只有当使用非唯一性索引或者唯一性索引的非唯一性前缀时才会发生。把它叫做 ref 是因为索引要跟某个参考值相比较。ref_or_null 是 ref 智商的一个变体,它意味着 MySQL 必须在初次查找的结果里进行第二次查找以找出
NULL
条目。 - eq_ref:使用这种所有查找,MySQL 知道最多只返回一条符合条件的查询。这种访问方法可以在 MySQL 使用主键或者唯一性索引查找时看到,它会将它们与某个参考值做比较。
- const, system:当 MySQL 能对查询的某部分进行优化并将其转换成一个常量时,它就会使用这些访问类型。举例来说,如果你通过将某一行的主键放入
WHERE
子句里的方式来选取此行的主键,MySQL 就能把这个查询转换为一个常量。然后就可以高效地将表从联接执行中移除。 - NULL:这种访问方式意味着 MySQL 能在优化阶段分解查询语句,在执行阶段甚至用不着再访问表或者索引。例如,从一个索引列里选取最小值可以通过单独查找索引来完成,不需要再执行时访问表。
- possible_keys: 显示了查询可以使用哪些索引,这是基于查询访问的列和使用的比较操作符来判断的。这个列表是在优化过程的早起创建的,因此有些罗列出来的索引可能对于后续优化过程是没用的。
- key: 显示了 MySQL 决定采用哪个索引来优化对该表的访问。如果该索引没有出现在 possible_keys 列中,那么 MySQL 选用它是出于另外的原因——例如,它可能选择了一个覆盖所有,哪怕没有
WHERE
子句。换句话说, possible_keys 揭示了哪一个索引能有助于高效地行查找,而 key 显示的是优化采用哪一个索引可以最小化查询成本。 - key_len: MySQL 在索引里使用的字节数。
- ref: 显示了之前表在 key 列记录的索引中查找值所用的列或常量。
- rows: MySQL 估计为了找到所需的行而要读取的行数。
- filtered: 针对表里符合某个条件(WHERE 子句或联接条件)的记录数的百分比所做的一个悲观估计。
- Extra: 包含的是不适用在其他列显示的额外信息。常见的最重要的值如下:
- Using index:使用了覆盖索引。
- Using where:MySQL 服务器将在存储引擎检索行后再进行过滤。许多
WHERE
条件里涉及索引中的列,当(并且如果)它读取索引时,就能被存储引擎检验,因此不是所有带WHERE
子句的查询都会显示 "Using where" 。有时 "Using where" 的出现就是一个暗示:查询可受益于不同的索引。 - Using temporary:意味着 MySQL 在对查询结果排序时会使用一个临时表。
- Using filesort:这意味着 MySQL 会在结果使用一个外部索引排序,而不是按索引次序从表里读取行。MySQL 有两种文件排序算法,两种方式都可以在内存或磁盘上完成。EXPLAIN 不会告诉你 MySQL 将使用哪一种文件排序,也不会告诉你排序会在内存里还是磁盘上完成。
- Range checked for each record (index map: N):这个值以为着没有好用的索引,新的索引将在联接的每一行上重新估计。N 是显示在 possible_keys 列中索引的位图,并且是冗余的。
更换 Blog 地址
就是为了更新起来比较方便,看着比较好看,还带 RSS ~
yield与yield from备忘录
对于 yield
与yield from
的语法,官方给出的解释Link
yield <expr>
yield from <expr>
等于
(yield <expr>)
(yield from <expr>)
要理解yield
需要先介绍几个概念
Iterables
(可迭代对象)
An object capable of returning its members one at a time. Examples of iterables include all sequence types (such as
list
,str
, andtuple
) and some non-sequence types likedict
, file objects, and objects of any classes you define with an__iter__()
or__getitem__()
method. Iterables can be used in afor
loop and in many other places where a sequence is needed (zip()
,map()
, ...).When an iterable object is passed as an argument to the built-in function
iter()
, it returns an iterator for the object. This iterator is good for one pass over the set of values. When using iterables, it is usually not necessary to calliter()
or deal with iterator objects yourself. Thefor
statement does that automatically for you, creating a temporary unnamed variable to hold the iterator for the duration of the loop. See also iterator, sequence, and generator.
>>> mylist = [x*x for x in range(3)]
>>> for i in mylist :
... print(i)
0
1
4
所有你可以使用 for .. in ..
语法的叫做一个迭代器:列表,字符串,文件……你经常使用它们是因为你可以如你所愿的读取其中的元素,但是你把所有的值都存储到了内存中,如果你有大量数据的话这个方式并不是你想要的。
Generators
(生成器)
A function which returns a generator iterator. It looks like a normal function except that it contains
yield
expressions for producing a series of values usable in a for-loop or that can be retrieved one at a time with thenext()
function.Usually refers to a generator function, but may refer to a generator iterator in some contexts. In cases where the intended meaning isn’t clear, using the full terms avoids ambiguity.
生成器是可以迭代的,但是你 只可以读取它一次 ,因为它并不把所有的值放在内存中,它是实时地生成数据:
>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator :
... print(i)
0
1
4
看起来除了把 []
换成 ()
外没什么不同。但是,你不可以再次使用 for i inmygenerator
, 因为生成器只能被迭代一次:先计算出0,然后继续计算1,然后计算4,一个跟一个的…
其实这里跟流的概念(计算机程序的构造与解释)是一样的,所谓流就是一个语法糖而已,如果用Python来实现,应该可以这么做:
class Generator:
def __init__(self, l):
self.l = l
def __iter__(self):
return self
def __next__(self):
if len(self.l) > 0:
return self.l.pop(0)
raise StopIteration()
In [1]: for i in Generator([1, 2, 3, 4]):
...: print(i)
...:
1
2
3
4
当然,我这只是简单范例,以后我会再结合Python源码来说明生成器是如何实现的(挖了个大坑)。
yield
语法
先给出官方文档连接(Link)
yield
是一个类似 return
的关键字,只是这个函数返回的是个生成器。
>>> def createGenerator() :
... mylist = range(3)
... for i in mylist :
... yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
... print(i)
0
1
4
这个例子没什么用途,但是它让你知道,这个函数会返回一大批你只需要读一次的值.
为了精通 yield
,你必须要理解:当你调用这个函数的时候,函数内部的代码并不立马执行 ,这个函数只是返回一个生成器对象,这有点蹊跷不是吗。
那么,函数内的代码什么时候执行呢?当你使用for进行迭代的时候.
现在到了关键点了!
第一次迭代中你的函数会执行,从开始到达 yield
关键字,然后返回 yield
后的值作为第一次迭代的返回值. 然后,每次执行这个函数都会继续执行你在函数内部定义的那个循环的下一次,再返回那个值,直到没有可以返回的。
如果生成器内部没有定义 yield
关键字,那么这个生成器被认为成空的。这种情况可能因为是循环进行没了,或者是没有满足 if/else
条件。
看到上述解释,你可以这样认为,yield
是实现一个语法糖,其语法是将这个函数本身传了出去,并生成一个对象,这个对象是一个生成器,其会执行一次yield
封装的函数然后将结果作为下一次的返回值(第一次的返回值是直接返回,或者这样更好实现,就是如果检测到函数有yield
关键字则直接封装为一个生成器对象,然后调用就是先执行一次yield
封装的函数,然后再返回数据)。
yield from
该语法是在PEP380中被定义的。总之大意是原本的yield语句只能将CPU控制权 还给直接调用者,当你想要将一个generator或者coroutine里带有 yield语句的逻辑重构到另一个generator(原文是subgenerator) 里的时候,会非常麻烦,因为外面的generator要负责为里面的 generator做消息传递;所以某人有个想法是让python把消息传递封装起来,使其对开发者透明,于是就有了yield from
。
yield from x()
其中的x应该返回一个可迭代的对象(Iterables
),这样整个表达式会返回一个生成器(Generators
),我们将整个表达式称为y(就是说y=lambda:yield from x()
)。
The full semantics of the yield from expression can be described in terms of the generator protocol as follows:
- Any values that the iterator yields(x) are passed directly to the caller(y).
- Any values sent to the delegating generator using send() are passed directly to the iterator. If the sent value is None, the iterator's __next__() method is called. If the sent value is not None, the iterator's send() method is called. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator.
- Exceptions other than GeneratorExit thrown into the delegating generator are passed to the throw() method of the iterator. If the call raises StopIteration, the delegating generator is resumed. Any other exception is propagated to the delegating generator.
- If a GeneratorExit exception is thrown into the delegating generator, or the close() method of the delegating generator is called, then the close() method of the iterator is called if it has one. If this call results in an exception, it is propagated to the delegating generator. Otherwise, GeneratorExit is raised in the delegating generator.
- The value of the yield from expression is the first argument to the StopIteration exception raised by the iterator when it terminates.
- return expr in a generator causes StopIteration(expr) to be raised upon exit from the generator.
In [1]: def reader():
...: """A generator that fakes a read from a file, socket, etc."""
...: for i in range(4):
...: yield '<< %s' % i
...:
In [2]: def reader_wrapper(g):
...: # Manually iterate over data produced by reader
...: for v in g:
...: yield v
...:
In [3]: wrap = reader_wrapper(reader())
...: for i in wrap:
...: print(i)
...:
<< 0
<< 1
<< 2
<< 3
In [4]: def reader_wrapper(g):
...: yield from g
...:
In [5]: wrap = reader_wrapper(reader())
...: for i in wrap:
...: print(i)
...:
<< 0
<< 1
<< 2
<< 3
这篇文章作为介绍Python的asyncio
的基础文章。
参考文章:
Spring 源码解析—Bean的加载前奏
Spring源码解析——Bean的加载前奏
User user = (User)context.getBean("testbean");
由这句入手
AbstractBeanFactory#getBean
@Override
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 提取对应的 beanName
final String beanName = transformedBeanName(name);
Object bean;
// Eagerly check singleton cache for manually registered singletons.
/**
检查缓存中或者实例工厂中是否有对应的实例
为什么首先会使用这段代码呢,因为在创建单例 bean 的时候会存在依赖注入的情况,
而在创建依赖的时候为了避免循环依赖,Spring 创建 bean 的原则是不等 bean 创建
完成就会将创建的 bean 的 ObjectFactory 提前曝光,也就是将 ObjectFactory 加入
缓存中,一旦下个 bean 创建时候需要依赖上个 bean 则直接使用 ObjectFactory
**/
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
if (logger.isDebugEnabled()) {
if (isSingletonCurrentlyInCreation(beanName)) {
logger.debug("Returning eagerly cached instance of singleton bean '" + beanName +
"' that is not fully initialized yet - a consequence of a circular reference");
}
else {
logger.debug("Returning cached instance of singleton bean '" + beanName + "'");
}
}
// 返回对应的实例,有时候存在诸如 BeanFactory 的情况并不是直接返回实例本身而是返回指定方法返回的实例
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// Fail if we're already creating this bean instance:
// We're assumably within a circular reference.
/**
只有在单例情况才会尝试解决依赖循环,原型模型情况,如果存在
A 中有 B 的熟悉,B 中有 A 的属性,那么当依赖注入的时候,就会产生当 A 还未创建完的时候
因为对于 B 的创建再次返回创建 ,造成依赖循环,也就是下面的情况
**/
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
// Check if bean definition exists in this factory.
BeanFactory parentBeanFactory = getParentBeanFactory();
// 如果 beanDefinitionMap 中也就是在所有已经加载的类中不包括 beanName 则
// 尝试从 parentBeanFactory 中检测
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
// 递归到 BeanFactory 中寻找
if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
// 如果不是仅仅做类型检查则是创建 bean,这里要进行记录
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}
try {
// 将存储 XML 配置文件的 GernericBeanDefinition 转换为 RootBeanDefinition,
// 如果指定 BeanName 是子 Bean 的话同时会合并父类的相关属性
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
// Guarantee initialization of beans that the current bean depends on.
String[] dependsOn = mbd.getDependsOn();
// 若存在依赖则需要递归实例化依赖的 bean
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");
}
// 缓存依赖调用
registerDependentBean(dep, beanName);
getBean(dep);
}
}
// Create bean instance.
// 实例化依赖的 bean 后便可以实例化 mbd 本身了
// singleton 模式的创建
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// It's a prototype -> create a new instance.
// prototype 模式的创建 (new)
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
else {
// 指定的 scope 上实例化 bean
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");
}
try {
Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
beforePrototypeCreation(beanName);
try {
return createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
}
});
bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName,
"Scope '" + scopeName + "' is not active for the current thread; consider " +
"defining a scoped proxy for this bean if you intend to refer to it from a singleton",
ex);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}
// Check if required type matches the type of the actual bean instance.
// 检查需要的类型是否符合 bean 的实际类型
if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {
try {
return getTypeConverter().convertIfNecessary(bean, requiredType);
}
catch (TypeMismatchException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
}
return (T) bean;
}
大致过程:
-
转换对应 beanName。这里的 beanName 可能是别名,可能是 FactoryBean,所以需要一系列的解析:
- 去除 FactoryBean 的修饰符,也就是如果
name="&aa"
,那么会首先去除&
使得name=aa
- 取指定 alias 所表示的最终 beanName,例如别名 A 指向名称为 B 的 bean 则返回 B;若别名 A 指向别名 B,别名 B 又指向名称为 C 的 bean 则返回 C
- 去除 FactoryBean 的修饰符,也就是如果
-
尝试从缓存中加载单例
单例在 Spring 的同一个容器内只会被创建一次,后续在获取 bean,就直接从单例缓存中获取了。当然这里也只是尝试加载,首先尝试从缓存中加载,如果加载不成功则再次尝试从 singletonFactories 中加载。
由于存在依赖注入的问题,所以在 Spring 中创建 bean 的原则是不等 bean 创建完成就会将创建的 ObjectFactory 提早曝光加入到缓存中,一旦下一个 bean 创建需要依赖上一个 bean 则直接使用 ObjectFactory。
-
bean 的实例化
-
原型模式的依赖检查
只有单例会尝试解决循环依赖。
-
在非 singleton 下检测 parentBeanFactory,看是否需要进入 parentBeanFactory 中加载(当前 BeanFactory 中无该 bean 且 parentBeanFactory 存在且存在该 bean)
-
将存储 XML 配置文件的 GernericBeanDefinition 转换为 RootBeanDefinition。方便 Bean 的后续处理。
-
寻找依赖
-
针对不同的 scope 进行 bean 的创建
-
类型转换(requiredType = true)
FactoryBean 的使用
一般来说,Spring 是通过反射机制利用 bean 的 class 属性指定实现类来实例化 bean 的。FactoryBean 是为了对付配置 bean 的复杂性的。
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
// 如果返回 true 则 getObject() 时候会将实例放入 Spring 容器中单实例缓存池中
boolean isSingleton();
}
实现了 XxxFactoryBean
之后,解析<bean id="xxx" class="xx.xx.XxxFactoryBean" />
时候会调用该其实现的 getObject()
方法
缓存中获取单例 bean
@Override
public Object getSingleton(String beanName) {
// 设置 true 表示允许早期依赖
return getSingleton(beanName, true);
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 如果为空,则锁定全局变量并进行处理
synchronized (this.singletonObjects) {
// 如果此 bean 正在加载则不处理
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 当某些方法需要提前初始化时候会调用 addSingletonFactory 方法
// 将对应的 ObjectFactory 初始化策略存储在 singletonFactories
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 调用预先设定的 getObject 方法
singletonObject = singletonFactory.getObject();
// 记录在缓存中,earlySingletonObjects 和 singletonFactories 互斥
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
首先尝试从 singletonObjects
中获取实例,如果获取不到,则从 earlySingletonObjects
里面获取,如果还取不到,则尝试从 singletonFactories
里面获取 beanName
对应的 ObjectFactory
,然后调用这个 ObjectFactory
的 getObject 来创建 bean,并放到 earlySingletonObjects
中,然后从 singletonFactories 中 remove 掉这个 ObjectFactory。
- singletonObjects: 用于保存 BeanName 和创建 bean 实例之间的关系,beanName -> beanInstance
- singletonFactories:用于保存 BeanName 和创建 bean 的工厂之间的关系,beanName -> ObjectFactory
- earlySingletonObjects:也是保存 BeanName 和创建 bean 实例之间的关系,与singletonObjects的不同之处在于,当一个单例bean被放到这里面后,那么当bean还在创建过程中,就可以通过getBean方法获取到了,其目的是用来检测循环引用。
- registeredSingletons:用来保存当前所有已注册的bean。
从 bean 的实例中获取对象
无论是从缓存中获取到的 bean 还是通过不同的 scope 策略加载的 bean 都只是最原始的 bean 状态,并不一定是我们最终想要的 bean。举个例子,假如我们需要对工厂 bean 进行处理,那么这里得到的其实是工厂 bean 的初始状态,但是我们真正需要的是工厂 bean 中定义的 factory-method 方法中返回的 bean ,而getObjectForBeanInstance
方法就是完成这个工作的。
protected Object getObjectForBeanInstance(
Object beanInstance, String name, String beanName, RootBeanDefinition mbd) {
// Don't let calling code try to dereference the factory if the bean isn't a factory.
// 如果指定的 name 是工厂相关(以 & 为前缀)且 beanInstance 又不是 FactoryBean 类型则验证不通过
if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) {
throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
}
// Now we have the bean instance, which may be a normal bean or a FactoryBean.
// If it's a FactoryBean, we use it to create a bean instance, unless the
// caller actually wants a reference to the factory.
// 现在我们有了个 bean 的实例,这个实例可能会是正常的 bean 或者 FactoryBean
// 如果是 FactoryBean 我们使用它创建实例,但如果用户想要直接获取工厂实例而不是工程对应的
// getObject 方法对应的实例那么传入的 name 应该包含前缀 &
if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
return beanInstance;
}
// 加载 FactoryBean
Object object = null;
if (mbd == null) {
// 尝试从缓存中加载 bean
object = getCachedObjectForFactoryBean(beanName);
}
if (object == null) {
// Return bean instance from factory.
// 到这里已经明确指定 beanInstance 一定是 FactoryBean 类型
FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
// Caches object obtained from FactoryBean if it is a singleton.
// containsBeanDefinition 检测 beanDefinitionMap 中也就是在所有已经加载的类中
// 检测是否定义 beanName
if (mbd == null && containsBeanDefinition(beanName)) {
// 将存储 XML 配置文件的 GernericBeanDefinition 转换为 RootBeanDefinition,
// 如果指定 BeanName 是子 Bean 的话同时会合并父类的相关属性
mbd = getMergedLocalBeanDefinition(beanName);
}
// 是否是用户定义而不是应用程序本身定义的
boolean synthetic = (mbd != null && mbd.isSynthetic());
object = getObjectFromFactoryBean(factory, beanName, !synthetic);
}
return object;
}
- 对 FactoryBean 正确性的验证
- 对非 FactoryBean 不做任何处理
- 对 bean 进行转换
- 将从 Factory 中解析 bean 的工作委托给
getObjectFromFactoryBean
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
synchronized (getSingletonMutex()) {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
object = doGetObjectFromFactoryBean(factory, beanName);
// Only post-process and store if not put there already during getObject() call above
// (e.g. because of circular reference processing triggered by custom getBean calls)
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
object = alreadyThere;
}
else {
if (object != null && shouldPostProcess) {
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName,
"Post-processing of FactoryBean's singleton object failed", ex);
}
}
this.factoryBeanObjectCache.put(beanName, (object != null ? object : NULL_OBJECT));
}
}
return (object != NULL_OBJECT ? object : null);
}
}
else {
Object object = doGetObjectFromFactoryBean(factory, beanName);
if (object != null && shouldPostProcess) {
try {
// 调用 ObjectFactory 的后处理器
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
}
}
return object;
}
}
这部分代码只是做了一件事:返回的 bean 如果是单例,那就必须要保证全局唯一,同时,也因为是单例的,所以不被重复创建,可以使用缓存来提高性能,也就是说已经加载过就要记录下来以便于下次复用,否则的话就直接获取了。
所以我们最后是在 doGetObjectFromFactoryBean
中看到了自己想要的方法
private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName)
throws BeanCreationException {
Object object;
try {
if (System.getSecurityManager() != null) {
AccessControlContext acc = getAccessControlContext();
try {
// 需要权限验证
object = AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
return factory.getObject();
}
}, acc);
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
// 直接调用 getObject 方法
object = factory.getObject();
}
}
catch (FactoryBeanNotInitializedException ex) {
throw new BeanCurrentlyInCreationException(beanName, ex.toString());
}
catch (Throwable ex) {
throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex);
}
// Do not accept a null value for a FactoryBean that's not fully
// initialized yet: Many FactoryBeans just return null then.
if (object == null && isSingletonCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(
beanName, "FactoryBean which is currently in creation returned null from getObject");
}
return object;
}
我们接下来看 doGetObjectFromFactoryBean
获取对象之后,最后返回对象的过程中操作 postProcessObjectFromFactoryBean
做了哪些工作?
AbstractAutowireCapableBeanFactory#postProcessObjectFromFactoryBean
protected Object postProcessObjectFromFactoryBean(Object object, String beanName) {
return applyBeanPostProcessorsAfterInitialization(object, beanName);
}
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
对于后处理器,后面来进行介绍。这里我们只需要了解在 Spring 获取 bean 的规则中有这样一条:尽可能保证所有 bean 初始化后都会调用注册的 BeanPostProcesser 的 postProcessAfterInitialization 方法进行处理,在世纪开发过程中可以根据此特性设计自己的逻辑业务。
获取单例
之前我们讲解了从缓存中获取单例的过程,那么,如果缓存中不存在已经加载的单例bean就需要从头开始bean的加载过程了,而Spring中使用getSingleton的重载方法实现bean的加载过程。
AbstractBeanFactory#getBean
片段
// 实例化依赖的 bean 后便可以实例化 mbd 本身了
// singleton 模式的创建
if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
DefaultSingletonBeanRegistry#getSingleton
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
// 全局变量需要同步
synchronized (this.singletonObjects) {
// 首先检查对应的 bean 是否已经加载过了,因为 singleton 模式其实就是复用以创建的 bean,
// 所以这步是必须的
Object singletonObject = this.singletonObjects.get(beanName); // 1
// 如果为空才可以进行 singleton 的 bean 的初始化
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
throw new BeanCreationNotAllowedException(beanName,
"Singleton bean creation not allowed while singletons of this factory are in destruction " +
"(Do not request a bean from a BeanFactory in a destroy method implementation!)");
}
if (logger.isDebugEnabled()) {
logger.debug("Creating shared instance of singleton bean '" + beanName + "'"); // 2
}
beforeSingletonCreation(beanName); // 3
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<Exception>();
}
try {
// 初始化 bean
singletonObject = singletonFactory.getObject(); // 4
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
afterSingletonCreation(beanName); // 5
}
if (newSingleton) {
// 加入缓存
addSingleton(beanName, singletonObject); // 6
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null); // 7
}
}
-
检查缓存是否已经加载过了
-
若没有加载,则记录 beanName 的正在加载状态
-
加载单例前记录加载状态,通过
this.singletonsCurrentlyInCreation.add(beanName)
,以便于对循环依赖进行检测protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } }
-
通过调用参数传入的
ObjectFactory
的个体Object
方法实例化bean
-
加载单例后的处理方法调用。当
bean
加载结束后需要移除缓存中对该bean
的正在加载状态的记录protected void afterSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) { throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation"); } }
-
将结果记录至缓存并删除加载
bean
过程中所记录的各种辅助状态protected void addSingleton(String beanName, Object singletonObject) { synchronized (this.singletonObjects) { this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT)); this.singletonFactories.remove(beanName); this.earlySingletonObjects.remove(beanName); this.registeredSingletons.add(beanName); } }
-
返回处理结果
虽然我们已经从外部了解了加载bean的逻辑架构,但现在我们还并没有开始对bean加载功能的探索,之前提到过, bean 的加载逻辑其实是在传入的 ObjectFactory 类型的参数singletonFactory中定义的,我们反推参数的获取,得到如下代码:
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// Explicitly remove instance from singleton cache: It might have been put there
// eagerly by the creation process, to allow for circular reference resolution.
// Also remove any beans that received a temporary reference to the bean.
destroySingleton(beanName);
throw ex;
}
}
});
ObjectFactory
的核心部分其实只是调用了 createBean
的方法,所以,继续~
准备创建 bean
AbstractAutowireCapableBeanFactory#createBean
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
if (logger.isDebugEnabled()) {
logger.debug("Creating instance of bean '" + beanName + "'");
}
RootBeanDefinition mbdToUse = mbd;
// Make sure bean class is actually resolved at this point, and
// clone the bean definition in case of a dynamically resolved Class
// which cannot be stored in the shared merged bean definition.
// 锁定 class,根据设置的 class 属性或者根据 className 来解析 Class
Class<?> resolvedClass = resolveBeanClass(mbd, beanName); // 1
// 如果解析成功则 clone RootBeanDefinition 并且设置其 bean 类为解析之后的 class
if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
mbdToUse = new RootBeanDefinition(mbd);
mbdToUse.setBeanClass(resolvedClass);
}
// Prepare method overrides.
// 验证及准备覆盖的方法
try {
mbdToUse.prepareMethodOverrides(); // 2
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(),
beanName, "Validation of method overrides failed", ex);
}
try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
// 给 BeanPostProcessors 一个机会来返回代理来替代真正的实例
Object bean = resolveBeforeInstantiation(beanName, mbdToUse); // 3
if (bean != null) {
return bean;
}
}
catch (Throwable ex) {
throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName,
"BeanPostProcessor before instantiation of bean failed", ex);
}
Object beanInstance = doCreateBean(beanName, mbdToUse, args); // 4
if (logger.isDebugEnabled()) {
logger.debug("Finished creating instance of bean '" + beanName + "'");
}
return beanInstance;
}
- 根据设置的 class 属性或者根据 className 来解析 Class
- 对 override 属性进行标记及验证(
lookup-method
andreplace-method
) - 应用初始化前的后处理器,解析指定 bean 是否存在初始化前的短路操作
- 创建 bean
处理 override
属性
public void prepareMethodOverrides() throws BeanDefinitionValidationException {
// Check that lookup methods exists.
MethodOverrides methodOverrides = getMethodOverrides();
if (!methodOverrides.isEmpty()) {
Set<MethodOverride> overrides = methodOverrides.getOverrides();
synchronized (overrides) {
for (MethodOverride mo : overrides) {
prepareMethodOverride(mo);
}
}
}
}
protected void prepareMethodOverride(MethodOverride mo) throws BeanDefinitionValidationException {
// 获取对应类中对应方法名的个数
int count = ClassUtils.getMethodCountForName(getBeanClass(), mo.getMethodName());
if (count == 0) {
throw new BeanDefinitionValidationException(
"Invalid method override: no method with name '" + mo.getMethodName() +
"' on class [" + getBeanClassName() + "]");
}
else if (count == 1) {
// Mark override as not overloaded, to avoid the overhead of arg type checking.
// 标记 MethoOverride 暂未被覆盖,避免参数类型检查的开销
mo.setOverloaded(false);
}
}
对于方法的匹配来讲,如果一个类中存在若干个重载方法,那么,在函数调用及增强的时候还需要根据参数类型进行匹配,来最终确认当前调用的到底是哪个函数。但是,Spring将一部分匹配工作在这里完成了,如果当前类中的方法只有一个,那么就设置重载该方法没有被重载,这样在后续调用的时候便可以直接使用找到的方法,而不需要进行方法的参数匹配验证了,而且还可以提前对方法存在性进行验证,正可谓一箭双雕。
实例化的前置处理
AOP基于前置处理后的短路判断
try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
// 给 BeanPostProcessors 一个机会来返回代理来替代真正的实例
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
// 提供一个短路判断,当经过处理之后的 bean 若不为空,则直接返回结果。
// 我们所熟知的 AOP 功能就是基于这里的判断
if (bean != null) {
return bean;
}
}
protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
Object bean = null;
// 如果尚未被解析
if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
// Make sure bean class is actually resolved at this point.
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
Class<?> targetType = determineTargetType(beanName, mbd);
if (targetType != null) {
bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
if (bean != null) {
bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
}
}
}
mbd.beforeInstantiationResolved = (bean != null);
}
return bean;
}
其中 applyBeanPostProcessorsBeforeInstantiation
与 applyBeanPostProcessorsAfterInitialization
分别对应实例化前后的处理器,实现也挺简单的,无非是对后处理器中的所有 InstantiationAwareBeanPostProcessor
类型的后处理器进行 postProcessBeforeInstantiation
方法和 BeanPostProcessor
的 postProcessAfterInitialization
方法的调用
实例化前的后处理器应用
protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
if (result != null) {
return result;
}
}
}
return null;
}
实例化后的后处理器应用
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
在讲解从缓存中获取单例bean的时候就提到过,Spring中的规则是在bean的初始化后尽可能保证将注册的后处理器的postProcessAfterInitialization方法应用到该bean中,因为如果返回的bean不为空,那么便不会再次经历普通bean的创建过程,所以只能在这里应用后处理器的postProcessAfterInitialization方法。
循环依赖
什么是循环依赖
循环依赖就是循环引用,就是两个或多个 bean 相互之间持有对方。循环依赖不是循环调用,循环调用是指方法之间的环调用的,循环调用除非有终止条件,否则无法解决。
Spring 如何解决循环依赖
我们先来定义一个循环引用类:
package io.github.binglau.circle;
/**
* 文件描述:
*/
public class TestA {
private TestB testB;
public void a() {
testB.b();
}
public TestB getTestB() {
return testB;
}
public void setTestB(TestB testB) {
this.testB = testB;
}
}
package io.github.binglau.circle;
/**
* 文件描述:
*/
public class TestB {
private TestC testC;
public void b() {
testC.c();
}
public TestC getTestC() {
return testC;
}
public void setTestC(TestC testC) {
this.testC = testC;
}
}
package io.github.binglau.circle;
/**
* 文件描述:
*/
public class TestC {
private TestA testA;
public void c() {
testA.a();
}
public TestA getTestA() {
return testA;
}
public void setTestA(TestA testA) {
this.testA = testA;
}
}
在 Spring 中将循环依赖的处理分成了 3 中情况
构造器循环依赖
无法解决,抛出 BeanCurrentlyInCreationException
异常
Spring容器将每一个正在创建的bean标识符放在一个“当前创建bean池”中,bean标识符在创建过程中将一直保持在这个池中,因此如果在创建 bean 过程中发现自己已经在“当前创建bean池”里时,将抛出BeanCurrentlyInCreationException异常表示循环依赖;而对于创建完毕的bean将从“当前创建bean池”中清除掉。
直观的测试
-
创建配置文件
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="testA" class="io.github.binglau.circle.TestA"> <constructor-arg index="0" ref="testB"/> </bean> <bean id="testB" class="io.github.binglau.circle.TestB"> <constructor-arg index="0" ref="testC"/> </bean> <bean id="testC" class="io.github.binglau.circle.TestC"> <constructor-arg index="0" ref="testA"/> </bean> </beans>
-
创建测试用例
@Test(expected = BeanCurrentlyInCreationException.class) public void testCircleByConstructor() throws Throwable { try { new ClassPathXmlApplicationContext("test.xml"); } catch (Exception e) { Throwable el = e.getCause().getCause().getCause(); throw el; } }
setter 循环依赖
表示通过 setter 注入方式构成的循环依赖。对于 setter 注入造成的依赖是通过 Spring 容器提前暴露刚完成构造器注入但未完成其他步骤(如 setter 注入)的 bean 来完成的,而且只能解决单例作用域的 bean 循环依赖。通过提前暴露一个单例工厂方法,从而使其他 bean 能引用到该 bean ,如下代码所示:
addSingletonFactory(beanName, new ObjectFactory() {
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
具体步骤如下:
- Spring 容器创建单例
testA
bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory
用于返回一个提前暴露一个创建中的 bean,并将testA
标识符放到 『当前创建 bean 池』,然后进行 setter 注入testB
。 - Spring 容器创建单例
testB
bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory
用于返回一个提前暴露一个创建中的 bean,并将“testB”标识符放到『当前创建bean池』,然后进行 setter 注入circle
。 - Spring 容器创建单例
testC
bean,首先根据无参构造器创建 bean,并暴露一个ObjectFactory
用于返回一个提前暴露一个创建中的 bean,并将testC
标识符放到『当前创建bean池』,然后进行setter注入testA
。进行注入testA
时由于提前暴露了ObjectFactory
工厂,从而使用它返回提前暴露一个创建中的 bean。 - 最后在依赖注入
testB
和testA
,完成 setter 注入。
prototype 范围的依赖处理
对于 prototype
作用域 bean,Spring 容器无法完成依赖注入,因为 Spring 容器不进行缓存 prototype
作用域的 bean,因此无法提前暴露一个创建中的 bean。
关于创建 bean 详见下篇文章
初学Kafka——Kafka实例及其讲解
基本概念
**Topic:**Kafka将消息种子(Feed)分门别类, 每一类的消息称之为话题(Topic).
**Producer:**发布消息的对象称之为话题生产者(Kafka topic producer)
**Consumer:**订阅消息并处理发布的消息的种子的对象称之为话题消费者(consumers)
**Broker:**已发布的消息保存在一组服务器中,称之为Kafka集群。集群中的每一个服务器都是一个代理(Broker). 消费者可以订阅一个或多个话题,并从Broker拉数据,从而消费这些已发布的消息。
**Partition:**每个Topic下会有一个或多个Partition,创建Topic时可指定Parition数量。每个Partition对应于一个文件夹,该文件夹下存储该{Partition的数据和索引文件。
Kafka就是讲生产者所生产的消息进行分类,而分类的条目就是Topic,最后由消费者接受消息的一个消息中间件。而消费者不是直接从生产者那里拿数据,而是从Broker从拉取数据,所谓Broker其实是一个服务器实例,负责接受从生产者那里来的数据,负责消息维护作用。
对于每个Topic,Kafka都会这一个分区的log,如图所示:
每一个分区都是一个顺序的、不可变的消息队列, 并且可以持续的添加。分区中的消息都被分配了一个序列号,称之为偏移量(offset),在每个分区中此偏移量都是唯一的(可以理解为Mysql中一张表的主键id)。Kafka集群保持所有的消息,直到它们过期,无论消息是否被消费了。实际上消费者所持有的仅有的元数据就是这个偏移量,也就是消费者在这个log中的位置。 这个偏移量由消费者控制:正常情况当消费者消费消息的时候,偏移量也线性的的增加。但是实际偏移量由消费者控制,消费者可以将偏移量重置为更老的一个偏移量,重新读取消息。可以看到这种设计对消费者来说操作自如, 一个消费者的操作不会影响其它消费者对此log的处理。
再说说分区。Kafka中采用分区的设计有几个目的:
- 是可以处理更多的消息,不受单台服务器的限制。Topic拥有多个分区意味着它可以不受限的处理更多的数据。
- 分区可以作为并行处理的单元。
命令行执行
我们可以先使用命令行才测试Kafka的简单操作。
先手动启动一个生产者者
>kafka-console-producer.sh --broker-list 192.168.33.10:9092 --topic test
This is message
在手动启动一个消费者
>kafka-console-consumer.sh --zookeeper 192.168.33.10:2181 --topic test --from-beginning
This is message //这就是收到的消息
这里要说明一下,我是使用Vagrant来做Kafka的服务器,连接时候会出现连接超时的情况,这时候需要调整kafka的配置中的advertised.host.name
,将其设置为我们为Vagrant的private_network
首先我这个kafka是在本机启动的,然后Vagrant是通过端口查询到的,然后kafka和zookeeper的端口是通过Vagrant映射来的,kafka是通过zookeeper来维护集群信息的,通过zookeeper找到Broker,然后尝试连接Broker。这个问题就是由于kafka发送的ip错误造成的,而advertised.host.name
设置是强行设置kafka的反馈ip。
Producer讲解
进行配置,主要有序列化的配置
public ProducerConfig config(){
Properties props = new Properties();
props.put("metadata.broker.list", "192.168.33.10:9092,192.168.33.10:9091"); //Broker集群位置,
props.put("serializer.class", "kafka.serializer.StringEncoder");//指定采用何种序列化的方式将消息传给Broker
props.put("partitioner.class", "io.github.binglau.BingPartitioner");//指定发送分区的对应方式,如果不指定则随机发送到某个分区,这里可以对分区进行一个负载均衡的操作
props.put("request.required.acks", "1");//指定消息是否确认发送成功
ProducerConfig config = new ProducerConfig(props);
return config;
}
定义Producer对象
Producer<String, String> producer = new Producer<String, String>(config);
发送消息
public void singleModeProduce(ProducerConfig config) throws InterruptedException{
Producer<String, String> producer = new Producer<String, String>(config);
for(int i = 0; i < 10000; i++){
producer.send(getMsg(i));
System.out.println(i);
Thread.sleep(1000);
}
}
public void batchModeProduce(ProducerConfig config){
List<KeyedMessage<String, String>> messages = new ArrayList<KeyedMessage<String, String>>(100);
Producer<String, String> producer = new Producer<String, String>(config);
for(int i = 0; i < 1000; i++){
messages.add(getMsg(i));
if (i % 100 == 0){
producer.send(messages);
messages.clear();
}
}
producer.send(messages);
}
HighLevelConsumer讲解
有时,我们消费Kafka的消息,并不关心偏移量,我们仅仅关心数据能被消费就行。High Level Consumer(高级消费者)提供了消费信息的方法而屏蔽了大量的底层细节。
设计一个高级消费者
了解使用高层次消费者的第一件事是,它可以(而且应该!)是一个多线程的应用。线程围绕在你的主题分区的数量,有一些非常具体的规则:
- 如果你提供比在topic分区多的线程数量,一些线程将永远不会看到消息。
- 如果你提供的分区比你拥有的线程多,线程将从多个分区接收数据。
- 如果你每个线程上有多个分区,对于你以何种顺序收到消息是没有保证的。举个例子,你可能从分区10上获取5条消息和分区11上的6条消息,然后你可能一直从10上获取消息,即使11上也拥有数据。
- 添加更多的进程线程将使kafka重新平衡,可能改变一个分区到线程的分配。
配置
private ConsumerConfig config(){
Properties props = new Properties();
props.put("group.id", groupId);
props.put("consumer.id", consumerId);
props.put("zookeeper.connect", Config.ZK_CONNECT);
props.put("zookeeper.session.timeout.ms", "60000");
props.put("zookeeper.sync.time.ms", "2000");
return new ConsumerConfig(props);
}
获取消息流
Map<String, List<KafkaStream<byte[], byte[]>>> streams = connector.createMessageStreams(topicCountMap);
创建线程处理消息
private class BingStreamThread implements Runnable{
private KafkaStream<byte[], byte[]> stream;
public BingStreamThread(KafkaStream<byte[], byte[]> stream){
this.stream = stream;
}
public void run() {
ConsumerIterator<byte[], byte[]> streamIterator = stream.iterator();
// 逐条处理消息
while (streamIterator.hasNext()) {
MessageAndMetadata<byte[], byte[]> message = streamIterator.next();
String topic = message.topic();
int partition = message.partition();
long offset = message.offset();
String key = new String(message.key());
String msg = new String(message.message());
// 在这里处理消息,这里仅简单的输出
// 如果消息消费失败,可以将已上信息打印到日志中,活着发送到报警短信和邮件中,以便后续处理
System.out.println("consumerId:" + consumerId
+ ", thread : " + Thread.currentThread().getName()
+ ", topic : " + topic
+ ", partition : " + partition
+ ", offset : " + offset
+ " , key : " + key
+ " , mess : " + msg);
}
}
}
消费消息
// 为每个stream启动一个线程消费消息
for (KafkaStream<byte[], byte[]> stream : streams.get(Config.TOPIC)) {
BingStreamThread bingStreamThread = new BingStreamThread(stream);
executor.submit(bingStreamThread);
}
SimpleConsumer讲解
为什么使用SimpleConsumer?
使用“SimpleConsumer”的主要原因是你想比使用“消费者分组”更好的控制分区消费。
比如你想:
1. 多次读取消息
2. 在一个处理过程中只消费Partition其中的一部分消息
3. 添加事务管理机制以保证消息被处理且仅被处理一次
使用SimpleConsumer有哪些弊端呢
这个SimpleConsumer确实需要很大的工作量:
1. 必须在程序中跟踪offset值.
2. 必须找出指定Topic(主题) Partition(分区)中的lead broker.
3. 必须处理broker的变动.
使用SimpleConsumer的步骤
找出Leader Broker
// 从所有活跃的broker中找出哪个是指定Topic(主题) Partition(分区)中的leader broker
BrokerEndPoint leaderBroker = findLeader(Config.KAFKA_ADDRESS, Config.TOPIC, partition);
/**
* 找到指定分区的leader broker
* @return 指定分区的leader broker
*/
public BrokerEndPoint findLeader(String brokerHosts, String topic, int partition) {
BrokerEndPoint leader = findPartitionMetadata(brokerHosts, topic, partition).leader();
System.out.println(String.format("Leader tor topic %s, partition %d is %s:%d",
topic, partition, leader.host(), leader.port()));
return leader;
}
/**
* 找到指定分区的元数据
* @return 指定分区元数据
*/
private PartitionMetadata findPartitionMetadata(String brokerHosts, String topic, int partition) {
PartitionMetadata returnMetadata = null;
for (String brokerHost : brokerHosts.split(",")) {
SimpleConsumer consumer = null;
try {
String[] splits = brokerHost.split(":");
consumer = new SimpleConsumer(
splits[0], Integer.valueOf(splits[1]), 100000, 64 * 1024, "leaderLookup"
);
List<String> topics = Collections.singletonList(topic);
TopicMetadataRequest request = new TopicMetadataRequest(topics);
TopicMetadataResponse response = consumer.send(request);
List<TopicMetadata> topicMetadatas = response.topicsMetadata();
for (TopicMetadata topicMetadata : topicMetadatas) {
for (PartitionMetadata partitionMetadata : topicMetadata.partitionsMetadata()) {
if (partitionMetadata.partitionId() == partition) {
returnMetadata = partitionMetadata;
break;
}
}
}
} catch (Exception e) {
System.out.println("Error communicating with Broker [" + brokerHost + "] to find Leader for [" + topic
+ ", " + partition + "] Reason: " + e);
} finally {
if (consumer != null) {
consumer.close();
}
}
}
return returnMetadata;
}
构造消费者
// 构造消费
SimpleConsumer simpleConsumer = new SimpleConsumer(
leaderBroker.host(), leaderBroker.port(), 20000, 10000, "bingSimpleConsumer");
long startOffset = 1;
int fetchSize = 1000;
构造请求与处理请求
while (true) {
long offset = startOffset;
// 构造请求
FetchRequest request = new FetchRequestBuilder()
.addFetch(Config.TOPIC, 0, startOffset, fetchSize).build();
// 发送请求获取数据
FetchResponse response = simpleConsumer.fetch(request);
ByteBufferMessageSet messageSet = response.messageSet(Config.TOPIC, partition);
for (MessageAndOffset messageAndOffset : messageSet) {
Message msg = messageAndOffset.message();
ByteBuffer payload = msg.payload();
byte[] bytes = new byte[payload.limit()];
payload.get(bytes);
String message = new String(bytes);
offset = messageAndOffset.offset();
System.out.println("partition : " + 3 + ", offset : " + offset + " msg : " + message);
}
// 继续消费下一批
startOffset = offset + 1;
}
参考资料
代码示例
Spring源码分析-bean的解析(1)
Spring源码分析-bean的解析(1)
当前版本 Spring 4.3.8
我们一开始需要先定义一个 Bean
和一个 xml
// bean
package io.github.binglau.bean;
import lombok.Data;
/**
* 文件描述:
*/
@Data // 简化 setter/getter
public class TestBean {
private String testStr = "test";
}
<!-- beanFactory.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="testBean" class="io.github.binglau.bean.TestBean" />
</beans>
这时候我们大多数是这么来启动 IoC 的
package io.github.binglau;
import io.github.binglau.bean.TestBean;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
/**
* 文件描述:
*/
public class BeanFactoryTest {
@Test
public void testSimpleLoad() {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:beanFactory.xml");
TestBean testBean = (TestBean) context.getBean("testBean");
System.out.printf("test bean: %s", testBean.getTestStr());
}
}
现在让我们来看看 ApplicationContext
代表了什么
package org.springframework.context;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.io.support.ResourcePatternResolver;
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
String getId();
String getApplicationName();
String getDisplayName();
long getStartupDate();
ApplicationContext getParent();
AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}
其实所谓的 getBean
方法是定义在 ListableBeanFactory
接口所继承的 BeanFactory
接口中的。这样说来,我们应该是可以直接通过 BeanFactory
来调用 Bean
的,其实有一个根据 xml
来实现的 BeanFactory
,是这样调用的:
package io.github.binglau;
import io.github.binglau.bean.TestBean;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
/**
* 文件描述:
*/
public class BeanFactoryTest {
@Test
public void testSimpleLoad() {
BeanFactory context = new XmlBeanFactory(new ClassPathResource("beanFactory.xml"));
// ApplicationContext context = new ClassPathXmlApplicationContext("classpath:beanFactory.xml");
TestBean testBean = (TestBean) context.getBean("testBean");
System.out.printf("test bean: %s", testBean.getTestStr());
}
}
好了,现在让我们开始进入 XmlBeanFactory
中来分析一下吧
XmlBeanFactory
package org.springframework.beans.factory.xml;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.core.io.Resource;
@Deprecated
@SuppressWarnings({"serial", "all"})
public class XmlBeanFactory extends DefaultListableBeanFactory {
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
public XmlBeanFactory(Resource resource) throws BeansException {
this(resource, null);
}
public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException {
super(parentBeanFactory);
this.reader.loadBeanDefinitions(resource);
}
}
这里我们可以看出,他是继承了 DefaultListableBeanFactory
,而 DefaultListableBeanFactory
也是整个 bean 加载的核心部分,是 Spring 注册及加载 bean 的默认实现。我们先从全局角度来了解一下它
DefaultListableBeanFactory
的各个类功能:
- AliasRegistry: 定义对 alias 的简单增删改等操作
- SimpleAliasRegistry: 主要使用 map 作为 alias 的缓存,并对接口 AliasRegistry 进行实现
- SingletonBeanRegistry: 定义对单例的注册及获取
- BeanFactory: 定义获取 bean 及 bean 的各种属性
- DefaultSingletonBeanRegistry: 对接口 SingletonBeanRegistry 各函数的实现
- HierarchicalBeanFactory: 继承 BeanFactory,也就是在 BeanFactory 定义的功能的基础上增加了对 parentFactory 的支持
- BeanDefinitionRegistry: 定义对 BeanDefinition 的各种增删改操作
- FactoryBeanRegistrySupport: 在 DefaultSingletonBeanRegistry 基础上增加了对 FactoryBean 的特殊处理功能
- ConfigurableBeanFactory: 提供配置 Factory 的各种方法
- ListableBeanFactory: 根据各种条件获取 bean 的配置清单
- AbstractBeanFactory: 综合 FactoryBeanRegistrySupport 和 ConfigurableBeanFactory 的功能
- AutowireCapableBeanFactory: 提供创建 bean、自动注入、初始化以及应用 bean 的后处理器
- AbstractAutowireCapableBeanFactory: 综合 AbstractBeanFactory 并对接口 AutowireCapableBeanFactory 进行实现
- ConfigurableListableBeanFactory: BeanFactory 配置清单,指定忽略类型及接口等
- DefaultListableBeanFactory: 综合上面的所有功能,主要是对 Bean 注册后的处理
XmlFactoryBean
主要是针对 XML 文档对 DefaultListableBeanFactory
的个性化实现,唯一不同的也就是 XmlBeanDefinitionReader
类型的 reader
。
XmlBeanDefinitionReader
其中我们看到 XmlFactoryBean
构造器中第二句 this.reader.loadBeanDefinitions(resource);
,但从名字来看它应该是加载 Bean 的主要执行者,而 reader 的定义在顶上 private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this);
。所谓 XmlBeanDefinitionReader
主要是负责读取 Spring 配置文件信息。
这张图 Idea 生成有些许差别,于是就自己画了一下。
- ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的 Resource
- BeanDefinitionReader: 主要定义资源文件读取并转换为 BeanDefinition 的各个功能
- EnvironmentCapable:定义获取 Environment 方法
- DocumentLoader:定义从资源文件加载到转换为 Document 的功能
- AbstractBeanDefinitionReader:对 EnvironmentCapable、BeanDefinitionReader 类定义的功能进行实现
- BeanDefinitionDocumentReader:定义读取 Document 并注册 BeanDefinition 功能
- BeanDefinitionParserDelegate: 定义解析 Element 的各种方法
经过上面的分析,我们大概能得出 XML 配置文件读取的大致流程:
- 通过继承自 AbstractBeanDefinitionReader 中的方法,来使用 ResourceLoader 将资源文件路径转换为对应的 Resource 文件
- 通过 DocumentLoader 对 Resource 文件进行转换,将 Resource 文件转换为 Document 文件
- 通过实现接口 BeanDefinitionDocumentReader 的 DefaultBeanDefinitionDocumentReader 类对 Document 进行解析,并使用 BeanDefinitionParserDelegate 对 Element 进行解析
分析 XmlBeanFactoy
这时候,让我们在回头看 BeanFactory context = new XmlBeanFactory(new ClassPathResource("beanFactory.xml"));
先从现有信息可以整理出下面这张时序图
配置文件封装
首先看看 Resource
配置文件的加载,也就是 new ClassPathResource("beanFactory.xml")
在 Java 中,将不同来源的资源抽象成 URL,通过注册不同的 handler(URLStreamHandler)
来处理不同来源的资源的读取逻辑,一般 handler 的类型使用不同前缀(协议,Protocol)来识别,如 “file:”、“http:”、“jar:” 等,然而 URL 没有默认定义相对 Classpath 或 ServletContext 等资源的 handler,虽然可以注册自己的 URLStreamHandler 来解析特定的URL前缀(协议),比如 “classpath:” ,然而这需要了解 URL 的实现机制,而且URL也没有提供一些基本的方法,如检查当前资源是否存在、检查当前资源是否可读等方法。 因而 Spring 对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源。
其内部资源的抽象结构:
public interface InputStreamSource {
/**
* Return an {@link InputStream} for the content of an underlying resource.
* <p>It is expected that each call creates a <i>fresh</i> stream.
* <p>This requirement is particularly important when you consider an API such
* as JavaMail, which needs to be able to read the stream multiple times when
* creating mail attachments. For such a use case, it is <i>required</i>
* that each {@code getInputStream()} call returns a fresh stream.
* @return the input stream for the underlying resource (must not be {@code null})
* @throws java.io.FileNotFoundException if the underlying resource doesn't exist
* @throws IOException if the content stream could not be opened
*/
InputStream getInputStream() throws IOException;
}
/**
InputSteramSource 抽象了所有 Spring 内部使用到的底层资源:File、URL、Classpath 下的资源和 ByteArray 等、它只有一个方法定义:getInputStream(),该方法返回一个新的 InputStream 对象。
**/
public interface Resource extends InputStreamSource {
/**
* 存在性
*/
boolean exists();
/**
* 可读性
*/
boolean isReadable();
/**
* 是否处于打开状态
*/
boolean isOpen();
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
long contentLength() throws IOException;
long lastModified() throws IOException;
/**
* 基于当前资源创建一个相对资源的方法
*/
Resource createRelative(String relativePath) throws IOException;
String getFilename();
/**
* 用于错误处理中的打印信息
*/
String getDescription();
}
对于不同来源的资源都有其对应的实现:
其中 ClassPathResouce
的实现
/**
* This implementation opens an InputStream for the given class path resource.
* @see java.lang.ClassLoader#getResourceAsStream(String)
* @see java.lang.Class#getResourceAsStream(String)
*/
@Override
public InputStream getInputStream() throws IOException {
InputStream is;
if (this.clazz != null) {
is = this.clazz.getResourceAsStream(this.path);
}
else if (this.classLoader != null) {
is = this.classLoader.getResourceAsStream(this.path);
}
else {
is = ClassLoader.getSystemResourceAsStream(this.path);
}
if (is == null) {
throw new FileNotFoundException(getDescription() + " cannot be opened because it does not exist");
}
return is;
}
在 XmlFactoryBean
中,我们先调用了 super
,现在看看 super
干了什么事情
/**
* Create a new AbstractAutowireCapableBeanFactory.
*/
public AbstractAutowireCapableBeanFactory() {
super();
ignoreDependencyInterface(BeanNameAware.class);
ignoreDependencyInterface(BeanFactoryAware.class);
ignoreDependencyInterface(BeanClassLoaderAware.class);
}
这里有必要提及一下 ignoreDependencyInterface
方法。ignoreDependencyInterface
的主要功能是忽略给定接口的自动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢?
举例来说,当 A 中有属性 B,那么当 Spring 在获取 A 的 Bean 的时候如果其属性 B 还没有初始化,那么 Spring 会自动初始化 B,这也是 Spring 中提供的一个重要特性。但是,某些情况下,B 不会被初始化,其中的一种情况就是 B 实现了 BeanNameAware 接口。Spring 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是通过其他方式解析 Application 上下文注册依赖,类似于 BeanFactory 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。
加载 Bean
this.reader.loadBeanDefinitions(resource)
的讲解,其时序图
- 封装资源文件。当进入
XmlBeanDefinitionReader
后首先对参数Resource
使用EncodedResource
类进行封装。 - 获取输入流。从
Resource
中获取对应的InputStream
并构造InputSource
- 通过构造的
InputSource
实例和Resource
实例继续调用函数doLoadBeanDefinitions
时序图中的逻辑实现
/**
* Load bean definitions from the specified XML file.
* @param encodedResource the resource descriptor for the XML file,
* allowing to specify an encoding to use for parsing the file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
*/
public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
Assert.notNull(encodedResource, "EncodedResource must not be null");
if (logger.isInfoEnabled()) {
logger.info("Loading XML bean definitions from " + encodedResource.getResource());
}
// 通过属性来记录已经加载的资源
Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
if (currentResources == null) {
currentResources = new HashSet<EncodedResource>(4);
this.resourcesCurrentlyBeingLoaded.set(currentResources);
}
if (!currentResources.add(encodedResource)) {
throw new BeanDefinitionStoreException(
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try {
// 从 encodedResource 中获取已经封装的 Resource 对象并再次从 Resource 中获取其中的 inputStream
InputStream inputStream = encodedResource.getResource().getInputStream();
try {
// InputSource 这个类并不来自于 Spring,它的全路径是 org.xml.sax.InputSource
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
// 真正进入逻辑核心部分
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
}
finally {
inputStream.close();
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"IOException parsing XML document from " + encodedResource.getResource(), ex);
}
finally {
currentResources.remove(encodedResource);
if (currentResources.isEmpty()) {
this.resourcesCurrentlyBeingLoaded.remove();
}
}
}
/**
* Actually load bean definitions from the specified XML file.
* @param inputSource the SAX InputSource to read from
* @param resource the resource descriptor for the XML file
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of loading or parsing errors
* @see #doLoadDocument
* @see #registerBeanDefinitions
*/
protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
throws BeanDefinitionStoreException {
try {
Document doc = doLoadDocument(inputSource, resource);
return registerBeanDefinitions(doc, resource);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (SAXParseException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"Line " + ex.getLineNumber() + " in XML document from " + resource + " is invalid", ex);
}
catch (SAXException ex) {
throw new XmlBeanDefinitionStoreException(resource.getDescription(),
"XML document from " + resource + " is invalid", ex);
}
catch (ParserConfigurationException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Parser configuration exception parsing XML from " + resource, ex);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"IOException parsing XML document from " + resource, ex);
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(resource.getDescription(),
"Unexpected exception parsing XML document from " + resource, ex);
}
}
/**
* Actually load the specified document using the configured DocumentLoader.
* @param inputSource the SAX InputSource to read from
* @param resource the resource descriptor for the XML file
* @return the DOM Document
* @throws Exception when thrown from the DocumentLoader
* @see #setDocumentLoader
* @see DocumentLoader#loadDocument
*/
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
doLoadBeanDefinitions
中做了三件事:
- 获取对 XML 文件的验证模型
- 加载 XML 文件,并得到对应的 Document
- 根据返回的 Document 注册 Bean 信息
获取 XML 的验证模式
DTD 与 XSD 区别
DTD (文档类型定义),保证 XML 文档格式正确的有效方法,可以通过比较 XML 文档和 DTD 文件来看文档是否符合规范,元素和标签使用是否正确。一个 DTD 文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符号规则。
XSD (XML Schemas Definition)。XML Scheme 描述了 XML 文档的结构。可以用一个指定的 XML Schema 来验证某个 XML 文档,以检查该 XML 文档是否符合其要求。文档设计者可以通过 XML Schema 指定一个 XML 文档所允许的结构和内容,并可据此检查一个 XML 文档是否是有效的。XML Schema 本身是一个 XML 文档,它符合 XML 语法结构。可以用通用的 XML 解析器解析它。
在使用 XML Schema 文档中 XML 实例文档进行检验,除了要声明名称空间外(xmlns = http://www.SpringFramework.org/schema/beans),还必须制定该名称空间所对应的 XML Schema 文档的存储位置。通过 schemaLocation 熟悉来指定名称空间所对应的 XML Schema 文档的存储位置,它包含两个部分,一部分是名称空的 URI,另一部分就是该名称空间所标识的 XML Shema 文件位置或 URL 地址(xsi:schemaLocation="http://www.Springframework.org/schema/beans http://www.Springframework.org/schema/beans/Spring-beans.xsd")
验证模式的获取
进入 Document doc = doLoadDocument(inputSource, resource);
方法
protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception {
return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler,
getValidationModeForResource(resource), isNamespaceAware());
}
其中getValidationModeForResource(resource)
就是获取验证模式。
/**
* Gets the validation mode for the specified {@link Resource}. If no explicit
* validation mode has been configured then the validation mode is
* {@link #detectValidationMode detected}.
* <p>Override this method if you would like full control over the validation
* mode, even when something other than {@link #VALIDATION_AUTO} was set.
*/
protected int getValidationModeForResource(Resource resource) {
int validationModeToUse = getValidationMode();
// 如果手动指定了验证模式则使用指定的验证模式
if (validationModeToUse != VALIDATION_AUTO) {
return validationModeToUse;
}
// 如果未指定则使用自动检测
int detectedMode = detectValidationMode(resource);
if (detectedMode != VALIDATION_AUTO) {
return detectedMode;
}
// Hmm, we didn't get a clear indication... Let's assume XSD,
// since apparently no DTD declaration has been found up until
// detection stopped (before finding the document's root tag).
return VALIDATION_XSD;
}
detectValidationMode
将自动检测验证模式工作委派给专门处理类 XmlValidationModeDetecotor
,调用了 XmlValidationModeDetecotor
的 vaildationModeDetector
方法,具体代码如下:
/**
* Detects which kind of validation to perform on the XML file identified
* by the supplied {@link Resource}. If the file has a {@code DOCTYPE}
* definition then DTD validation is used otherwise XSD validation is assumed.
* <p>Override this method if you would like to customize resolution
* of the {@link #VALIDATION_AUTO} mode.
*/
protected int detectValidationMode(Resource resource) {
if (resource.isOpen()) {
throw new BeanDefinitionStoreException(
"Passed-in Resource [" + resource + "] contains an open stream: " +
"cannot determine validation mode automatically. Either pass in a Resource " +
"that is able to create fresh streams, or explicitly specify the validationMode " +
"on your XmlBeanDefinitionReader instance.");
}
InputStream inputStream;
try {
inputStream = resource.getInputStream();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Unable to determine validation mode for [" + resource + "]: cannot open InputStream. " +
"Did you attempt to load directly from a SAX InputSource without specifying the " +
"validationMode on your XmlBeanDefinitionReader instance?", ex);
}
try {
return this.validationModeDetector.detectValidationMode(inputStream);
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("Unable to determine validation mode for [" +
resource + "]: an error occurred whilst reading from the InputStream.", ex);
}
}
/**
* Detect the validation mode for the XML document in the supplied {@link InputStream}.
* Note that the supplied {@link InputStream} is closed by this method before returning.
* @param inputStream the InputStream to parse
* @throws IOException in case of I/O failure
* @see #VALIDATION_DTD
* @see #VALIDATION_XSD
*/
public int detectValidationMode(InputStream inputStream) throws IOException {
// Peek into the file to look for DOCTYPE.
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
try {
boolean isDtdValidated = false;
String content;
while ((content = reader.readLine()) != null) {
content = consumeCommentTokens(content);
// 如果读取的行是空或者是注释则略过
if (this.inComment || !StringUtils.hasText(content)) {
continue;
}
if (hasDoctype(content)) {
isDtdValidated = true;
break;
}
// 读取到 < 开始符号,验证模式一定会在开始符号之前
if (hasOpeningTag(content)) {
// End of meaningful data...
break;
}
}
return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD);
}
catch (CharConversionException ex) {
// Choked on some character encoding...
// Leave the decision up to the caller.
return VALIDATION_AUTO;
}
finally {
reader.close();
}
}
/**
* Does the content contain the DTD DOCTYPE declaration?
*/
private boolean hasDoctype(String content) {
return content.contains(DOCTYPE);
}
获取 Document
/**
* Load the {@link Document} at the supplied {@link InputSource} using the standard JAXP-configured
* XML parser.
*/
@Override
public Document loadDocument(InputSource inputSource, EntityResolver entityResolver,
ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception {
DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware);
if (logger.isDebugEnabled()) {
logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]");
}
DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler);
return builder.parse(inputSource);
}
EntityResolver
解释: 如果 SAX 应用程序需要实现自定义处理外部实体,则必须实现此接口并使用 setEntityResolver
方法向 SAX 驱动器注册一个实例。即防止下载 DTD 时候网络错误,提供一个寻找 DTD 声明的方法。
解析及注册 BeanDefinitions
/**
* Register the bean definitions contained in the given DOM document.
* Called by {@code loadBeanDefinitions}.
* <p>Creates a new instance of the parser class and invokes
* {@code registerBeanDefinitions} on it.
* @param doc the DOM document
* @param resource the resource descriptor (for context information)
* @return the number of bean definitions found
* @throws BeanDefinitionStoreException in case of parsing errors
* @see #loadBeanDefinitions
* @see #setDocumentReaderClass
* @see BeanDefinitionDocumentReader#registerBeanDefinitions
*/
public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
// 使用 DefaultBeanDefinitionDocumentReader 实例化 BeanDefinitionDocumentReader
BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
// 在实例化 BeanDefinitionReader 时候会将 BeanDefinitionRegistry 传入,默认使用继承 DefaultListableBeanFactory 的子类
// 记录统计前 BeanDefinition 的加载个数
int countBefore = getRegistry().getBeanDefinitionCount();
// 加载及注册 bean
documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
// 记录本次加载的 BeanDefinition 个数
return getRegistry().getBeanDefinitionCount() - countBefore;
}
/**
* This implementation parses bean definitions according to the "spring-beans" XSD
* (or DTD, historically).
* <p>Opens a DOM Document; then initializes the default settings
* specified at the {@code <beans/>} level; then parses the contained bean definitions.
*/
@Override
public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) {
this.readerContext = readerContext;
logger.debug("Loading bean definitions");
Element root = doc.getDocumentElement();
// 重点:提取 root
doRegisterBeanDefinitions(root);
}
/**
* Register each bean definition within the given root {@code <beans/>} element.
*/
protected void doRegisterBeanDefinitions(Element root) {
// Any nested <beans> elements will cause recursion in this method. In
// order to propagate and preserve <beans> default-* attributes correctly,
// keep track of the current (parent) delegate, which may be null. Create
// the new (child) delegate with a reference to the parent for fallback purposes,
// then ultimately reset this.delegate back to its original (parent) reference.
// this behavior emulates a stack of delegates without actually necessitating one.
// 专门处理解析
BeanDefinitionParserDelegate parent = this.delegate;
this.delegate = createDelegate(getReaderContext(), root, parent);
if (this.delegate.isDefaultNamespace(root)) {
// 处理 profile 属性
String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
if (StringUtils.hasText(profileSpec)) {
String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
if (logger.isInfoEnabled()) {
logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
"] not matching: " + getReaderContext().getResource());
}
return;
}
}
}
// 解析前处理,留给子类实现
preProcessXml(root);
parseBeanDefinitions(root, this.delegate);
// 解析后处理,留给子类实现
postProcessXml(root);
this.delegate = parent;
}
/**
* Parse the elements at the root level in the document:
* "import", "alias", "bean".
* @param root the DOM root element of the document
*/
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// 对 beans 的处理
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
// 默认标签解析
parseDefaultElement(ele, delegate);
}
else {
// 自定义标签解析
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
参考书籍
《Spring源码深度解析》
apache 的 ab 工具简单介绍
首先我们看看 ab -h
命令提供的信息:
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform # 请求数
-c concurrency Number of multiple requests to make at a time # 并发请求数
-t timelimit Seconds to max. to spend on benchmarking # 测试所进行的最大秒数。
This implies -n 50000 # 其内部隐含值是-n 50000
-s timeout Seconds to max. wait for each response # 响应等待超时时间
Default is 30 seconds # 默认是 30 秒
-b windowsize Size of TCP send/receive buffer, in bytes # TCP窗口大小
-B address Address to bind to when making outgoing connections # 代理地址?
-p postfile File containing data to POST. Remember also to set -T # post请求包含的文件,也可以使用 -T
-u putfile File containing data to PUT. Remember also to set -T # put求包含的文件,也可以使用 -T
-T content-type Content-type header to use for POST/PUT data, eg. # post / put 请求时候 content-type 请求头
'application/x-www-form-urlencoded'
Default is 'text/plain'
-v verbosity How much troubleshooting info to print # 是否需要打印故障日志
-w Print out results in HTML tables # 以HTML表的格式输出结果
-i Use HEAD instead of GET # 使用 HEAD 方法代替 GET 方法
-x attributes String to insert as table attributes # 设置<table>属性的字符串。
-y attributes String to insert as tr attributes # 设置<tr>属性的字符串。
-z attributes String to insert as td or th attributes # 设置<td>属性的字符串。
-C attribute Add cookie, eg. 'Apache=1234'. (repeatable) # 对请求附加一个Cookie:行。其典型形式是name=value的一个参数对,此参数可以重复。
-H attribute Add Arbitrary header line, eg. 'Accept-Encoding: gzip' #对请求附加额外的头信息。此参数的典型形式是一个有效的头信息行,其中包含了以冒号分隔的字段和值的对(如,"Accept-Encoding:zip/zop;8bit")。
Inserted after all normal header lines. (repeatable)
-A attribute Add Basic WWW Authentication, the attributes # 对服务器提供BASIC认证信任。
are a colon separated username and password.
-P attribute Add Basic Proxy Authentication, the attributes # 对一个中转代理提供BASIC认证信任。
are a colon separated username and password.
-X proxy:port Proxyserver and port number to use # 对请求使用代理服务器。
-V Print version number and exit # 显示版本号
-k Use HTTP KeepAlive feature # 启用HTTP KeepAlive功能,即在一个HTTP会话中执行多个请求。默认时,不启用KeepAlive功能。
-d Do not show percentiles served table. # 不显示"percentage served within XX [ms] table"的消息(为以前的版本提供支持)。
-S Do not show confidence estimators and warnings. # 不显示估计配置与警告
-q Do not show progress when doing more than 150 requests # 如果处理的请求数大于150,ab每处理大约10%或者100个请求时,会在stderr输出一个进度计数。此-q标记可以抑制这些信息。
-l Accept variable document length (use this for dynamic pages) # 接受可变长度的文档
-g filename Output collected data to gnuplot format file. # 通过 gnuplot 文件格式收集输出数据
-e filename Output CSV file with percentages served # 产生一个以逗号分隔的(CSV)文件,其中包含了处理每个相应百分比的请求所需要(从1%到100%)的相应百分比的(以微妙为单位)时间。由于这种格式已经“二进制化”,所以比'gnuplot'格式更有用。
-r Don't exit on socket receive errors. # 当 socket 返回错误时不退出
-m method Method name # 请求方法名称
-h Display usage information (this message) # 显示帮助信息
-Z ciphersuite Specify SSL/TLS cipher suite (See openssl ciphers) # 指定 SSL/TLS 加密方式
-f protocol Specify SSL/TLS protocol # 指定 SSL/TLS 协议
(SSL3, TLS1, TLS1.1, TLS1.2 or ALL)
接下来我们使用 http://www.douban.com/
作为测试网站
➜ ~ ab -n 100 -c 100 http://www.douban.com/
This is ApacheBench, Version 2.3 <$Revision: 1604373 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking www.douban.com (be patient).....done
Server Software: dae # 测试服务器软件名称
Server Hostname: www.douban.com # 测试网址Host
Server Port: 80 # 测试端口
Document Path: / # 测试Path
Document Length: 160 bytes # 返回的报文大小
Concurrency Level: 100 # 标识并发的用户数
Time taken for tests: 0.284 seconds # 执行完所有的请求所花费的时间
Complete requests: 100 # 完成请求数
Failed requests: 0 # 失败请求数
Non-2xx responses: 100 # 返回响应非2xx数
Total transferred: 30100 bytes # 整个过程中的网络传输量
HTML transferred: 16000 bytes # 整个过程中的HTML大小
Requests per second: 352.02 [#/sec] (mean) # 吞吐率,表示每秒处理的请求数,mean标识平均值
Time per request: 284.075 [ms] (mean) # 每个用户等待时间,mean标识平均值
Time per request: 2.841 [ms] (mean, across all concurrent requests) #服务器平均请求处理的时间.
Transfer rate: 103.47 [Kbytes/sec] received # 这些请求在单位内,从服务器获取的数据长度.
Connection Times (ms) # 网络上消耗的时间的分解
min mean[+/-sd] median max
Connect: 26 37 7.9 35 63
Processing: 44 130 76.7 88 239
Waiting: 44 129 76.5 88 239
Total: 70 167 77.5 133 277
Percentage of the requests served within a certain time (ms) # 整个场景中所有请求的响应情况。
50% 133
66% 240
75% 271
80% 272
90% 275
95% 277
98% 277
99% 277
100% 277 (longest request)
Spring 源码解析—创建 bean
创建 bean
在经历过 AbstractAutowireCapableBeanFactory#createBean
中的 resolveBeforeInstantiation
方法后,程序有两个选择,如果创建了代理或者说重写了 InstantiationAwareBeanPostProcessor
的 postProcessBeforeInstantiation
方法并在方法 postProcessBeforeInstantiation
中改变了 bean
,则直接返回就可以了,否则需要进行常规 bean
的创建。而这常规 bean
的创建就是在 doCreateBean
中完成的。
AbstractAutowireCapableBeanFactory#doCreateBean
/**
* Actually create the specified bean. Pre-creation processing has already happened
* at this point, e.g. checking {@code postProcessBeforeInstantiation} callbacks.
* <p>Differentiates between default bean instantiation, use of a
* factory method, and autowiring a constructor.
* @param beanName the name of the bean
* @param mbd the merged bean definition for the bean
* @param args explicit arguments to use for constructor or factory method invocation
* @return a new instance of the bean
* @throws BeanCreationException if the bean could not be created
* @see #instantiateBean
* @see #instantiateUsingFactoryMethod
* @see #autowireConstructor
*/
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) { // 1
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) { // 2
// 根据指定 bean 使用对应的策略创建新的实例,如:工厂方法、构造函数自动注入、简单初始化
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
mbd.resolvedTargetType = beanType;
// Allow post-processors to modify the merged bean definition.
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
// 应用 MergedBeanDefinitionPostProcessor
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); // 3
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// 4
// 是否需要提前曝光:单例 & 允许循环依赖 & 当前 bean 正在创建中,检测循环依赖
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 为了避免后期循环依赖,可以在 bean 初始化完成前将创建实例的 ObjectFactory 加入工厂
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// 对 bean 再一次依赖引用,主要应用 SmartInstantiationAware BeanPostProcessor,
// 其中我们熟知的 AOP 就是在这里将 advice 动态织入 bean 中,若没有直接返回 bean,不做任何处理
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
// Initialize the bean instance.
Object exposedObject = bean;
try {
// 对 bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 bean 的属性,则会递归初始依赖 bean
// 5
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
// 调用初始化方法,比如 init-method
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
}
}
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
// earlySingletonReference 只有在检测到有循环依赖的情况下才会不为空
// 6
if (earlySingletonReference != null) {
// 如果 esposedObject 没有在初始化方法中被改变,就是没有被增强
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<String>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
// 检测依赖
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
// 因为 bean 创建后其所依赖的 bean 一定是已经创建的,
// actualDependentBeans 不为空则表示当前 bean 创建后其依赖的 bean 却没有
// 全部创建完,也就是说存在循环依赖
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(beanName,
"Bean with name '" + beanName + "' has been injected into other beans [" +
StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
"] in its raw version as part of a circular reference, but has eventually been " +
"wrapped. This means that said other beans do not use the final version of the " +
"bean. This is often the result of over-eager type matching - consider using " +
"'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
}
}
}
}
// Register bean as disposable.
try {
// 根据 scopes 注册 bean
registerDisposableBeanIfNecessary(beanName, bean, mbd); // 7
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
}
return exposedObject; // 8
}
-
如果是单例则需要首先清除缓存
-
实例化 bean,将
BeanDefinition
转换为BeanWrapper
转换是一个复杂的过程,但是我们可以尝试概括大致的功能:
- 如果存在工厂方法则使用工厂方法进行初始化
- 一个类有多个构造函数,每个构造函数都有不同的参数,所以需要根据参数锁定构造函数并进行初始化
- 如果既不存在工厂方法也不存在带有参数的构造函数,则使用默认的构造函数进行 bean 的实例化
-
MergedBeanDefinitionPostProcessor的应用
bean合并后的处理,Autowired注解正是通过此方法实现诸如类型的预解析。
-
依赖处理
在Spring中会有循环依赖的情况,例如,当A中含有B的属性,而B中又含有A的属性时就会构成一个循环依赖,此时如果A和B都是单例,那么在Spring中的处理方式就是当创建B的时候,涉及自动注入A的步骤时,并不是直接去再次创建A,而是通过放入缓存中的ObjectFactory来创建实例,这样就解决了循环依赖的问题。
-
属性填充。将所有属性填充至bean的实例中
-
循环依赖检查
Sping 中解决循环依赖只对单例有效,而对于 prototype 的 bean,Spring 没有好的解决办法,唯一要做的就是抛出异常。在这个步骤里面会检测已经加载的 bean 是否已经出现了依赖循环,并判断是否需要抛出异常。
-
注册DisposableBean
如果配置了destroy-method,这里需要注册以便于在销毁时候调用。
-
完成创建并返回
创建 bean 的实例 (createBeanInstance)
/**
* Create a new instance for the specified bean, using an appropriate instantiation strategy:
* factory method, constructor autowiring, or simple instantiation.
* @param beanName the name of the bean
* @param mbd the bean definition for the bean
* @param args explicit arguments to use for constructor or factory method invocation
* @return BeanWrapper for the new instance
* @see #instantiateUsingFactoryMethod
* @see #autowireConstructor
* @see #instantiateBean
*/
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
// Make sure bean class is actually resolved at this point.
// 解析 class
Class<?> beanClass = resolveBeanClass(mbd, beanName);
if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
}
// 如果工厂方法不为空则使用工厂方法初始化策略
if (mbd.getFactoryMethodName() != null) { // 1
return instantiateUsingFactoryMethod(beanName, mbd, args);
}
// 2
// Shortcut when re-creating the same bean...
boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
synchronized (mbd.constructorArgumentLock) {
// 一个类有多个构造函数,每个构造函数都有不同的参数,所以调用前需要先根据参数锁定构造函数或对应的工厂方法
if (mbd.resolvedConstructorOrFactoryMethod != null) {
resolved = true;
autowireNecessary = mbd.constructorArgumentsResolved;
}
}
}
// 如果已经解析过则使用解析好的构造函数方法不需要再次锁定
if (resolved) {
if (autowireNecessary) {
// 构造函数自动注入
return autowireConstructor(beanName, mbd, null, null);
}
else {
// 使用默认构造函数构造
return instantiateBean(beanName, mbd);
}
}
// Need to determine the constructor...
// 需要根据参数解析构造函数
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null ||
mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
// 构造函数自动注入
return autowireConstructor(beanName, mbd, ctors, args);
}
// No special handling: simply use no-arg constructor.
// 使用默认构造函数构造
return instantiateBean(beanName, mbd);
}
- 如果在
RootBeanDefinition
中存在factoryMethodName
属性,或者说在配置文件中配置了factory-method
,那么 Spring 会尝试使用instantiateUsingFactoryMethod(beanName, mbd, args)
方法根据RootBeanDefinition
中的配置生成bean的实例。 - 解析构造函数并进行构造函数的实例化。因为一个 bean 对应的类中可能会有多个构造函数,而每个构造函数的参数不同,Spring 在根据参数及类型去判断最终会使用哪个构造函数进行实例化。但是,判断的过程是个比较消耗性能的步骤,所以采用缓存机制,如果已经解析过则不需要重复解析而是直接从
RootBeanDefinition
中的属性resolvedConstructorOrFactoryMethod
缓存的值去取,否则需要再次解析,并将解析的结果添加至RootBeanDefinition
中的属性resolvedConstructorOrFactoryMethod
中。
### autowireConstructor
对于实例的创建Spring中分成了两种情况,一种是通用的实例化,另一种是带有参数的实例化。带有参数的实例化过程相当复杂,因为存在着不确定性,所以在判断对应参数上做了大量工作。
/**
* "autowire constructor" (with constructor arguments by type) behavior.
* Also applied if explicit constructor argument values are specified,
* matching all remaining arguments with beans from the bean factory.
* <p>This corresponds to constructor injection: In this mode, a Spring
* bean factory is able to host components that expect constructor-based
* dependency resolution.
* @param beanName the name of the bean
* @param mbd the bean definition for the bean
* @param ctors the chosen candidate constructors
* @param explicitArgs argument values passed in programmatically via the getBean method,
* or {@code null} if none (-> use constructor argument values from bean definition)
* @return BeanWrapper for the new instance
*/
protected BeanWrapper autowireConstructor(
String beanName, RootBeanDefinition mbd, Constructor<?>[] ctors, Object[] explicitArgs) {
return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs);
}
这边的代码量太大了,不太适合贴出来解析,各位最好还是去 idea 里面进行查看,书中作者也觉得这段不符合 Spring 那种『将复杂的逻辑分解,分成N个小函数的嵌套,每一层都是对下一层逻辑的总结及概要,这样使得每一层的逻辑会变得简单容易理解』的规律。我这里就说明一下整体流程,每段流程贴出对应的代码。
构造函数参数的确定
-
根据explicitArgs参数判断
如果传入的参数
explicitArgs
不为空,那边可以直接确定参数,因为explicitArgs
参数是在调用 Bean 的时候用户指定的,在BeanFactory
类中存在这样的方法:
Object getBean(String name, Object... args) throws BeansException;
在获取 bean 的时候,用户不但可以指定 bean 的名称还可以指定 bean 所对应类的构造函数或者工厂方法的方法参数,主要用于静态工厂方法的调用,而这里是需要给定完全匹配的参数的,所以,便可以判断,如果传入参数explicitArgs
不为空,则可以确定构造函数参数就是它。// explicitArgs 通过 getBean 方法传入 // 如果 getBean 方法调用的时候指定方法参数那么直接使用 if (explicitArgs != null) { argsToUse = explicitArgs }
-
缓存中获取
构造函数参数已经记录在缓存中,那么便可以直接拿来使用。而且,这里要提到的是,在缓存中缓存的可能是参数的最终类型也可能是参数的初始类型,例如:构造函数参数要求的是 int 类型,但是原始的参数值可能是String类型的“1”,那么即使在缓存中得到了参数,也需要经过类型转换器的过滤以确保参数类型与对应的构造函数参数类型完全对应。
else { // 如果在 getBean 方法时候没有指定则尝试从配置文件中解析 Object[] argsToResolve = null; // 尝试从缓存中获取 synchronized (mbd.constructorArgumentLock) { constructorToUse = (Constructor<?>) mbd.resolvedConstructorOrFactoryMethod; if (constructorToUse != null && mbd.constructorArgumentsResolved) { // Found a cached constructor... // 从缓存中取 argsToUse = mbd.resolvedConstructorArguments; if (argsToUse == null) { // 配置的构造函数参数 argsToResolve = mbd.preparedConstructorArguments; } } } // 如果缓存中存在 if (argsToResolve != null) { // 解析参数类型,如给定方法的构造函数 A(int, int) 则通过此方法后就会把配置汇中的 ("1", "1") 转换为 (1, 1) // 缓存中的值可能是原始值也可能是最终值 argsToUse = resolvePreparedArguments(beanName, mbd, bw, constructorToUse, argsToResolve); } }
-
配置文件获取
如果不能根据传入的参数
explicitArgs
确定构造函数的参数也无法在缓存中得到相关信息,那么只能开始新一轮的分析了。
分析从获取配置文件中配置的构造函数信息开始,经过之前的分析,我们知道,Spring中配置文件中的信息经过转换都会通过BeanDefinition
实例承载,也就是参数mbd
中包含,那么可以通过调用mbd.getConstructorArgumentValues()
来获取配置的构造函数信息。有了配置中的信息便可以获取对应的参数值信息了,获取参数值的信息包括直接指定值,如:直接指定构造函数中某个值为原始类型 String 类型,或者是一个对其他 bean 的引用,而这一处理委托给resolveConstructorArguments
方法,并返回能解析到的参数的个数。// Need to resolve the constructor. boolean autowiring = (chosenCtors != null || mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR); ConstructorArgumentValues resolvedValues = null; int minNrOfArgs; if (explicitArgs != null) { minNrOfArgs = explicitArgs.length; } else { // 提取配置文件中的配置的构造函数参数 ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues(); // 用于承载解析后的构造函数参数的值 resolvedValues = new ConstructorArgumentValues(); // 能解析到的参数个数 minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues); }
构造函数的确认
经过了第一步后已经确定了构造函数的参数,接下来的任务就是根据构造函数参数在所有构造函数中锁定对应的构造函数,而匹配的方法就是根据参数个数匹配,所以在匹配之前需要先对构造函数按照public构造函数优先参数数量降序、非public构造函数参数数量降序。这样可以在遍历的情况下迅速判断排在后面的构造函数参数个数是否符合条件。
// Take specified constructors, if any.
Constructor<?>[] candidates = chosenCtors;
if (candidates == null) {
Class<?> beanClass = mbd.getBeanClass();
try {
candidates = (mbd.isNonPublicAccessAllowed() ?
beanClass.getDeclaredConstructors() : beanClass.getConstructors());
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Resolution of declared constructors on bean Class [" + beanClass.getName() +
"] from ClassLoader [" + beanClass.getClassLoader() + "] failed", ex);
}
}
// 排序给定的构造函数,public 构造函数优先参数数量排序、非 public 构造函数参数数量降序
AutowireUtils.sortConstructors(candidates);
由于在配置文件中并不是唯一限制使用参数位置索引的方式去创建,同样还支持指定参数名称进行设定参数值的情况,如<constructor-arg name="aa">
,那么这种情况就需要首先确定构造函数中的参数名称。
获取参数名称可以有两种方式
-
通过注解的方式直接获取,
String[] paramNames = ConstructorPropertiesChecker.evaluate(candidate, paramTypes.length);
-
使用Spring中提供的工具类
ParameterNameDiscoverer
来获取。构造函数、参数名称、参数类型、参数值都确定后就可以锁定构造函数以及转换对应的参数类型了。if (paramNames == null) { ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); if (pnd != null) { paramNames = pnd.getParameterNames(candidate); } }
根据确定的构造函数转换对应的参数类型
主要是使用Spring中提供的类型转换器或者用户提供的自定义类型转换器进行转换。
argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,
getUserDeclaredConstructor(candidate), autowiring);
构造参数不确定性的验证
当然,有时候即使构造函数、参数名称、参数类型、参数值都确定后也不一定会直接锁定构造函数,不同构造函数的参数为父子关系,所以Spring在最后又做了一次验证。
// 探测是否有不确定性的构造函数存在,例如不同构造函数的参数为父子关系
int typeDiffWeight = (mbd.isLenientConstructorResolution() ?
argsHolder.getTypeDifferenceWeight(paramTypes) : argsHolder.getAssignabilityWeight(paramTypes));
// Choose this constructor if it represents the closest match.
// 如果它代表着当前最接近的匹配则选择作为构造函数
if (typeDiffWeight < minTypeDiffWeight) {
constructorToUse = candidate;
argsHolderToUse = argsHolder;
argsToUse = argsHolder.arguments;
minTypeDiffWeight = typeDiffWeight;
ambiguousConstructors = null;
}
else if (constructorToUse != null && typeDiffWeight == minTypeDiffWeight) {
if (ambiguousConstructors == null) {
ambiguousConstructors = new LinkedHashSet<Constructor<?>>();
ambiguousConstructors.add(constructorToUse);
}
ambiguousConstructors.add(candidate);
}
根据实例化策略以及得到的构造函数及构造函数参数实例化Bean
if (System.getSecurityManager() != null) {
final Constructor<?> ctorToUse = constructorToUse;
final Object[] argumentsToUse = argsToUse;
beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
return beanFactory.getInstantiationStrategy().instantiate(
mbd, beanName, beanFactory, ctorToUse, argumentsToUse);
}
}, beanFactory.getAccessControlContext());
}
else {
beanInstance = this.beanFactory.getInstantiationStrategy().instantiate(
mbd, beanName, this.beanFactory, constructorToUse, argsToUse);
}
后面章节进行描述。
instantiateBean
不带参数的构造函数实例化过程
if (resolved) {
if (autowireNecessary) {
return autowireConstructor(beanName, mbd, null, null);
}
else {
return instantiateBean(beanName, mbd);
}
}
/**
* Instantiate the given bean using its default constructor.
* @param beanName the name of the bean
* @param mbd the bean definition for the bean
* @return BeanWrapper for the new instance
*/
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
try {
Object beanInstance;
final BeanFactory parent = this;
if (System.getSecurityManager() != null) {
beanInstance = AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
return getInstantiationStrategy().instantiate(mbd, beanName, parent);
}
}, getAccessControlContext());
}
else {
beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
}
BeanWrapper bw = new BeanWrapperImpl(beanInstance);
initBeanWrapper(bw);
return bw;
}
catch (Throwable ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}
实例化策略
其实,经过前面的分析,我们已经得到了足以实例化的所有相关信息,完全可以使用最简单的反射方法直接反射来构造实例对象,但是Spring却并没有这么做。
SimpleInstantiationStrategy#instantiate
@Override
public Object instantiate(RootBeanDefinition bd, String beanName, BeanFactory owner) {
// Don't override the class with CGLIB if no overrides.
// 如果有需要覆盖或者动态替换的方法则当然需要使用 cglib 进行动态代理,因为可以在
// 创建代理的同时将方法将动态方法织入类中,但是如果没有需要动态改变的方法,为了
// 方便直接反射就可以了
if (bd.getMethodOverrides().isEmpty()) {
Constructor<?> constructorToUse;
synchronized (bd.constructorArgumentLock) {
constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
if (constructorToUse == null) {
final Class<?> clazz = bd.getBeanClass();
if (clazz.isInterface()) {
throw new BeanInstantiationException(clazz, "Specified class is an interface");
}
try {
if (System.getSecurityManager() != null) {
constructorToUse = AccessController.doPrivileged(new PrivilegedExceptionAction<Constructor<?>>() {
@Override
public Constructor<?> run() throws Exception {
return clazz.getDeclaredConstructor((Class[]) null);
}
});
}
else {
constructorToUse = clazz.getDeclaredConstructor((Class[]) null);
}
bd.resolvedConstructorOrFactoryMethod = constructorToUse;
}
catch (Throwable ex) {
throw new BeanInstantiationException(clazz, "No default constructor found", ex);
}
}
}
return BeanUtils.instantiateClass(constructorToUse);
}
else {
// Must generate CGLIB subclass.
return instantiateWithMethodInjection(bd, beanName, owner);
}
}
CglibSubclassingInstantiationStrategy#instantiate
/**
* An inner class created for historical reasons to avoid external CGLIB dependency
* in Spring versions earlier than 3.2.
*/
private static class CglibSubclassCreator {
private static final Class<?>[] CALLBACK_TYPES = new Class<?>[]
{NoOp.class, LookupOverrideMethodInterceptor.class, ReplaceOverrideMethodInterceptor.class};
private final RootBeanDefinition beanDefinition;
private final BeanFactory owner;
CglibSubclassCreator(RootBeanDefinition beanDefinition, BeanFactory owner) {
this.beanDefinition = beanDefinition;
this.owner = owner;
}
/**
* Create a new instance of a dynamically generated subclass implementing the
* required lookups.
* @param ctor constructor to use. If this is {@code null}, use the
* no-arg constructor (no parameterization, or Setter Injection)
* @param args arguments to use for the constructor.
* Ignored if the {@code ctor} parameter is {@code null}.
* @return new instance of the dynamically generated subclass
*/
public Object instantiate(Constructor<?> ctor, Object... args) {
Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
Object instance;
if (ctor == null) {
instance = BeanUtils.instantiateClass(subclass);
}
else {
try {
Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes());
instance = enhancedSubclassConstructor.newInstance(args);
}
catch (Exception ex) {
throw new BeanInstantiationException(this.beanDefinition.getBeanClass(),
"Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex);
}
}
// SPR-10785: set callbacks directly on the instance instead of in the
// enhanced class (via the Enhancer) in order to avoid memory leaks.
Factory factory = (Factory) instance;
factory.setCallbacks(new Callback[] {NoOp.INSTANCE,
new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
return instance;
}
/**
* Create an enhanced subclass of the bean class for the provided bean
* definition, using CGLIB.
*/
private Class<?> createEnhancedSubclass(RootBeanDefinition beanDefinition) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(beanDefinition.getBeanClass());
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
if (this.owner instanceof ConfigurableBeanFactory) {
ClassLoader cl = ((ConfigurableBeanFactory) this.owner).getBeanClassLoader();
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl));
}
enhancer.setCallbackFilter(new MethodOverrideCallbackFilter(beanDefinition));
enhancer.setCallbackTypes(CALLBACK_TYPES);
return enhancer.createClass();
}
}
记录创建 bean 的 ObjectFactory
在 doCreate
函数中有这样一段代码:
// Eagerly cache singletons to be able to resolve circular references
// even when triggered by lifecycle interfaces like BeanFactoryAware.
// 4
// 是否需要提前曝光:单例 & 允许循环依赖 & 当前 bean 正在创建中,检测循环依赖
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
if (logger.isDebugEnabled()) {
logger.debug("Eagerly caching bean '" + beanName +
"' to allow for resolving potential circular references");
}
// 为了避免后期循环依赖,可以在 bean 初始化完成前将创建实例的 ObjectFactory 加入工厂
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
// 对 bean 再一次依赖引用,主要应用 SmartInstantiationAware BeanPostProcessor,
// 其中我们熟知的 AOP 就是在这里将 advice 动态织入 bean 中,若没有直接返回 bean,不做任何处理
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}
-
mbd.isSingleton
: 是否是单例 -
this.allowCircularReferences
: 是否允许循环依赖,很抱歉,并没有找到在配置文件中如何配置,但是在 AbstractRefreshableApplicationContext 中提供了设置函数,可以通过硬编码的方式进行设置或者可以通过自定义命名空间进行配置,其中硬编码的方式代码如下:ClassPathXmlApplicationContext bf = new ClassPathXmlApplicationContext ("aspectTest.xml"); bf.setAllowBeanDefinitionOverriding(false);
-
isSingletonCurrentlyInCreation(beanName)
: 该bean是否在创建中。在Spring中,会有个专门的属性默认为DefaultSingletonBeanRegistry
的singletonsCurrentlyInCreation
来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将 beanName 从属性中移除。记录状态的点,不同 scope 的位置不一样,以
singleton
为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry
类的public Object getSingleton(String beanName, ObjectFactory singletonFacotry)
函数的beforeSingletonCreation(beanName)
和afterSingletonCreation(beanName)
中,在这两段函数中分别是this.singletonsCurrentlyInCreation.add(beanName)
与this.singletonsCurrentlyInCreation.remove(beanName)
来进行状态的记录与移除。
属性注入
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
/**
* Populate the bean instance in the given BeanWrapper with the property values
* from the bean definition.
* @param beanName the name of the bean
* @param mbd the bean definition for the bean
* @param bw BeanWrapper with bean instance
*/
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
PropertyValues pvs = mbd.getPropertyValues();
if (bw == null) {
if (!pvs.isEmpty()) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance");
}
else {
// Skip property population phase for null instance.
// 没有可填充的属性
return;
}
}
// Give any InstantiationAwareBeanPostProcessors the opportunity to modify the
// state of the bean before properties are set. This can be used, for example,
// to support styles of field injection.
// 给 InstantiationAwareBeanPostprocessors 最优一次机会在属性设置前来改变 bean
// 如:可以用来支持属性注入的类型
boolean continueWithPropertyPopulation = true;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) { // 1
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 返回值为是否继续填充 bean
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}
// 如果后处理器发出停止填充命令则终止后续的运行
if (!continueWithPropertyPopulation) {
return;
}
// 2
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||
mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
// 根据名称自动注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
// 根据类型自动注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
// 后处理器已经初始化
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
// 需要依赖检查
boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);
if (hasInstAwareBpps || needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
if (hasInstAwareBpps) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) { // 3
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
// 对所有需要依赖检查的属性进行后处理
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
// 依赖检查,对应 depends-on 属性, 3.0 已经弃用此属性
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
// 将属性应用到 bean 中
applyPropertyValues(beanName, mbd, bw, pvs); // 4
}
InstantiationAwareBeanPostProcessor
处理器的postProcessAfterInstantiation
函数的应用,此函数可以控制程序是否继续进行属性填充。- 根据注入类型(byName/byType),提取依赖的 bean,并统一存入
PropertyValues
中。 - 应用
InstantiationAwareBeanPostProcessor
处理器的postProcessPropertyValues
方法,对属性获取完毕填充前对属性的再次处理,典型应用是RequiredAnnotationBeanPostProcessor
类中对属性的验证。 - 将所有
PropertyValues
中的属性填充至BeanWrapper
中。
在上面的步骤中有几个地方是我们比较感兴趣的,它们分别是依赖注入( autowireByName
/autowireByType
)以及属性填充,那么,接下来进一步分析这几个功能的实现细节。
autowireByName
protected void autowireByName(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
// 寻找 bw 中需要依赖注入的属性
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
if (containsBean(propertyName)) {
// 递归初始化相关的 bean
Object bean = getBean(propertyName);
pvs.add(propertyName, bean);
// 注册依赖
registerDependentBean(propertyName, beanName);
if (logger.isDebugEnabled()) {
logger.debug("Added autowiring by name from bean name '" + beanName +
"' via property '" + propertyName + "' to bean named '" + propertyName + "'");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName +
"' by name: no matching bean found");
}
}
}
}
autowireByType
protected void autowireByType(
String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
TypeConverter converter = getCustomTypeConverter();
if (converter == null) {
converter = bw;
}
Set<String> autowiredBeanNames = new LinkedHashSet<String>(4);
// 寻找 bw 中需要依赖注入的属性
String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
for (String propertyName : propertyNames) {
try {
PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
// Don't try autowiring by type for type Object: never makes sense,
// even if it technically is a unsatisfied, non-simple property.
if (Object.class != pd.getPropertyType()) {
// 探测指定属性的 set 方法
MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd);
// Do not allow eager init for type matching in case of a prioritized post-processor.
boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass());
DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager);
Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
// 解析指定 beanName 的属性所匹配的值,并把解析到的属性名称存储在
// autowireBeanNames 中,当属性存在多个封装 bean 时,如:
// @Autowired private List<A> aList; 将会找到所有匹配 A 类型
// 的 bean 并将其注入
if (autowiredArgument != null) {
pvs.add(propertyName, autowiredArgument);
}
for (String autowiredBeanName : autowiredBeanNames) {
// 注册依赖
registerDependentBean(autowiredBeanName, beanName);
if (logger.isDebugEnabled()) {
logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" +
propertyName + "' to bean named '" + autowiredBeanName + "'");
}
}
autowiredBeanNames.clear();
}
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
}
}
}
现根据名称自动匹配的第一步就是寻找 bw
中需要依赖注入的属性,同样对于根据类型自动匹配的实现来讲第一步也是寻找bw
中需要依赖注入的属性,然后遍历这些属性并寻找类型匹配的 bean,其中最复杂的就是寻找类型匹配的 bean。
DefaultListableBeanFactory#resolveDependency
public Object resolveDependency(DependencyDescriptor descriptor, String requestingBeanName,
Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (javaUtilOptionalClass == descriptor.getDependencyType()) {
return new OptionalDependencyFactory().createOptionalDependency(descriptor, requestingBeanName);
}
else if (ObjectFactory.class == descriptor.getDependencyType() ||
ObjectProvider.class == descriptor.getDependencyType()) {
// ObjectFactory 类注入的特殊处理
return new DependencyObjectProvider(descriptor, requestingBeanName);
}
else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
// javaxInjectProviderClass 类注入的特殊处理
return new Jsr330ProviderFactory().createDependencyProvider(descriptor, requestingBeanName);
}
else {
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
// 通用逻辑处理
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}
}
public Object doResolveDependency(DependencyDescriptor descriptor, String beanName,
Set<String> autowiredBeanNames, TypeConverter typeConverter) throws BeansException {
InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
try {
Object shortcut = descriptor.resolveShortcut(this);
if (shortcut != null) {
return shortcut;
}
Class<?> type = descriptor.getDependencyType();
// 用于支持 Spring 中的注解 @Value
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
if (value instanceof String) {
String strVal = resolveEmbeddedValue((String) value);
BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
value = evaluateBeanDefinitionString(strVal, bd);
}
TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
return (descriptor.getField() != null ?
converter.convertIfNecessary(value, type, descriptor.getField()) :
converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
Object multipleBeans = resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);
if (multipleBeans != null) {
return multipleBeans;
}
// 根据属性类型找到 beanFactory 中所有类型的匹配 bean,
// 返回值的构成为: key: 匹配的 beanName, value: beanName 对应的实例化后的 bean(通过 getBean(beanName) 返回)
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
if (matchingBeans.isEmpty()) {
// 如果 autowire 的 require 属性为 true 而找到的匹配项却为空则只能抛出异常
if (descriptor.isRequired()) {
raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);
}
return null;
}
String autowiredBeanName;
Object instanceCandidate;
if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (descriptor.isRequired() || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(type, matchingBeans);
}
else {
// In case of an optional Collection/Map, silently ignore a non-unique case:
// possibly it was meant to be an empty collection of multiple regular beans
// (before 4.3 in particular when we didn't even look for collection beans).
return null;
}
}
instanceCandidate = matchingBeans.get(autowiredBeanName);
}
else {
// We have exactly one match.
Map.Entry<String, Object> entry = matchingBeans.entrySet().iterator().next();
autowiredBeanName = entry.getKey();
instanceCandidate = entry.getValue();
}
if (autowiredBeanNames != null) {
autowiredBeanNames.add(autowiredBeanName);
}
return (instanceCandidate instanceof Class ?
descriptor.resolveCandidate(autowiredBeanName, type, this) : instanceCandidate);
}
finally {
ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);
}
}
applyPropertyValues
程序运行到这里,已经完成了对所有注入属性的获取,但是获取的属性是以PropertyValues
形式存在的,还并没有应用到已经实例化的bean中,这一工作是在applyPropertyValues
中。
/**
* Apply the given property values, resolving any runtime references
* to other beans in this bean factory. Must use deep copy, so we
* don't permanently modify this property.
* @param beanName the bean name passed for better exception information
* @param mbd the merged bean definition
* @param bw the BeanWrapper wrapping the target object
* @param pvs the new property values
*/
protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) {
if (pvs == null || pvs.isEmpty()) {
return;
}
MutablePropertyValues mpvs = null;
List<PropertyValue> original;
if (System.getSecurityManager() != null) {
if (bw instanceof BeanWrapperImpl) {
((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext());
}
}
if (pvs instanceof MutablePropertyValues) {
mpvs = (MutablePropertyValues) pvs;
// 如果 mpvs 中的值已经被转换为对应的类型那么可以直接设置到 beanWapper 中
if (mpvs.isConverted()) {
// Shortcut: use the pre-converted values as-is.
try {
bw.setPropertyValues(mpvs);
return;
}
catch (BeansException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Error setting property values", ex);
}
}
original = mpvs.getPropertyValueList();
}
else {
// 如果 pvs 并不是使用 MutablePropertyValues 封装的类型,那么直接使用原始的属性获取方法
original = Arrays.asList(pvs.getPropertyValues());
}
TypeConverter converter = getCustomTypeConverter();
if (converter == null) {
converter = bw;
}
// 获取对应的解析器
BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter);
// Create a deep copy, resolving any references for values.
List<PropertyValue> deepCopy = new ArrayList<PropertyValue>(original.size());
boolean resolveNecessary = false;
// 遍历属性,将属性转换为对应类的对应属性的类型
for (PropertyValue pv : original) {
if (pv.isConverted()) {
deepCopy.add(pv);
}
else {
String propertyName = pv.getName();
Object originalValue = pv.getValue();
Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);
Object convertedValue = resolvedValue;
boolean convertible = bw.isWritableProperty(propertyName) &&
!PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);
if (convertible) {
convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter);
}
// Possibly store converted value in merged bean definition,
// in order to avoid re-conversion for every created bean instance.
if (resolvedValue == originalValue) {
if (convertible) {
pv.setConvertedValue(convertedValue);
}
deepCopy.add(pv);
}
else if (convertible && originalValue instanceof TypedStringValue &&
!((TypedStringValue) originalValue).isDynamic() &&
!(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) {
pv.setConvertedValue(convertedValue);
deepCopy.add(pv);
}
else {
resolveNecessary = true;
deepCopy.add(new PropertyValue(pv, convertedValue));
}
}
}
if (mpvs != null && !resolveNecessary) {
mpvs.setConverted();
}
// Set our (possibly massaged) deep copy.
try {
bw.setPropertyValues(new MutablePropertyValues(deepCopy));
}
catch (BeansException ex) {
throw new BeanCreationException(
mbd.getResourceDescription(), beanName, "Error setting property values", ex);
}
}
初始化 bean
大家应该记得在 bean 配置时 bean 中有一个init-method
的属性,这个属性的作用是在 bean 实例化前调用init-method
指定的方法来根据用户业务进行相应的实例化。我们现在就已经进入这个方法了,首先看一下这个方法的执行位置,Spring中程序已经执行过bean的实例化,并且进行了属性的填充,而就在这时将会调用用户设定的初始化方法。
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
invokeAwareMethods(beanName, bean);
return null;
}
}, getAccessControlContext());
}
else {
// 对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware
invokeAwareMethods(beanName, bean);
}
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 应用后处理器
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
// 激活用户自定义的 init 方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null || !mbd.isSynthetic()) {
// 后处理器应用
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
激活 Aware 方法
Spring 中提供一些 Aware 相关接口,比如 BeanFactoryAware
、ApplicationContextAware
、ResourceLoaderAware
、ServletContextAware
等,实现这些 Aware 接口的 bean 在被初始之后,可以取得一些相对应的资源,例如实现BeanFactoryAware
的 bean 在初始后, Spring 容器将会注入 BeanFactory
的实例,而实现ApplicationContextAware
的bean,在bean被初始后,将会被注入ApplicationContext
的实例等。
Aware 的使用
-
定义 bean
public class Hello { public void say() { System.out.println("hello"); } }
-
定义
BeanFactoryAware
类型的 beanpublic class TestAware implements BeanFactoryAware { private BeanFactory beanFactory; // 声明 bean 的时候 Spring 会自动注入 BeanFactory @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } public void testAware() { // 通过 hello 这个 bean id 从 beanFactory 获取实例 Hello hello = (Hello) beanFactory.getBean("hello"); hello.say(); } }
-
测试
public static void main(String[] args) { ApplicationContext ctx = new ClassPathXmlApplicationContext("testAware.xml"); TestAware testAware = (TestAware) ctx.getBean("testAware"); testAware.testAware(); } /** hello **/
invokeAwareMethods
private void invokeAwareMethods(final String beanName, final Object bean) {
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}
处理器的应用
BeanPostProcessor
相信大家都不陌生,这是 Spring 中开放式架构中一个必不可少的亮点,给用户充足的权限去更改或者扩展 Spring ,而除了 BeanPostProcessor
外还有很多其他的 PostProcessor
,当然大部分都是以此为基础,继承自 BeanPostProcessor
。BeanPostProcessor
的使用位置就是这里,在调用客户自定义初始化方法前以及调用自定义初始化方法后分别会调用 BeanPostProcessor
的 postProcessBeforeInitialization
和postProcessAfterInitialization
方法,使用户可以根据自己的业务需求进行响应的处理。
@Override
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessBeforeInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
@Override
public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
throws BeansException {
Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}
激活自定义的 init 方法
客户定制的初始化方法除了我们熟知的使用配置init-method
外,还有使自定义的 bean 实现InitializingBean
接口,并在afterPropertiesSet
中实现自己的初始化业务逻辑。
init-method
与afterPropertiesSet
都是在初始化 bean 时执行,执行顺序是afterPropertiesSet
先执行,而init-method
后执行。
在invokeInitMethods
方法中就实现了这两个步骤的初始化方法调用。
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd)
throws Throwable {
// 首先会检查是否是 InitializingBean,如果是的话需要调用 afterPropertiesSet 方法
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (logger.isDebugEnabled()) {
logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
}
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
((InitializingBean) bean).afterPropertiesSet();
return null;
}
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
// 属性初始化后的处理
((InitializingBean) bean).afterPropertiesSet();
}
}
if (mbd != null) {
String initMethodName = mbd.getInitMethodName();
if (initMethodName != null && !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&
!mbd.isExternallyManagedInitMethod(initMethodName)) {
// 调用自定义初始化方法
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
注册 DisposableBean
Spring 中不但提供了对于初始化方法的扩展入口,同样也提供了销毁方法的扩展入口,对于销毁方法的扩展,除了我们熟知的配置属性destroy-method
方法外,用户还可以注册后处理器DestructionAwareBeanPostProcessor
来统一处理bean的销毁方法,代码如下(doCreateBean
中):
protected void registerDisposableBeanIfNecessary(String beanName, Object bean, RootBeanDefinition mbd) {
AccessControlContext acc = (System.getSecurityManager() != null ? getAccessControlContext() : null);
if (!mbd.isPrototype() && requiresDestruction(bean, mbd)) {
if (mbd.isSingleton()) {
// Register a DisposableBean implementation that performs all destruction
// work for the given bean: DestructionAwareBeanPostProcessors,
// DisposableBean interface, custom destroy method.
// 单例模式下注册需要销毁的 bean,此方法中会处理实现 DisposableBean 的 bean,
// 并且堆所有的 bean 使用 DestructionAwareBeanPostProcessors 处理
// DisposableBean DestructionAwareBeanPostProcessors
registerDisposableBean(beanName,
new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
}
else {
// A bean with a custom scope...
// 自定义 scope 的处理
Scope scope = this.scopes.get(mbd.getScope());
if (scope == null) {
throw new IllegalStateException("No Scope registered for scope name '" + mbd.getScope() + "'");
}
scope.registerDestructionCallback(beanName,
new DisposableBeanAdapter(bean, beanName, mbd, getBeanPostProcessors(), acc));
}
}
}
Akka 实例
Akka 实例
动态路由器
动态路由器会使用规则,这些规则具有一定的复杂性。
要接收来自动态路由器的消息,Actor 对象必须注册它感兴趣的消息,通过最基础的注册流程,Actor 对象可以告诉路由器,它对哪种消息感兴趣。也可以通过更精细的规则,要求动态路由器执行多层次的查询操作,以便为 Actor 对象传输指定的消息。
// TypedMessageInterestRouter.scala
package io.binglau.scala.akka.demo.dynamic_router
import reflect.runtime.currentMirror
import akka.actor.{Actor, ActorRef}
import scala.collection._
case class InterestedIn(messageType: String)
case class NoLongerInterestedIn(messageType: String)
case class TypeAMessage(description: String)
case class TypeBMessage(description: String)
case class TypeCMessage(description: String)
case class TypeDMessage(description: String)
// 动态路由操作
class TypedMessageInterestRouter(dunnoInterested: ActorRef,
canStartAfterRegistered: Int,
canCompleteAfterUnregistered: Int) extends Actor {
// 主要接收者
val interestRegistry: mutable.Map[String, ActorRef] = mutable.Map[String, ActorRef]()
// 次要接收者
val secondaryInterestRegistry: mutable.Map[String, ActorRef] = mutable.Map[String, ActorRef]()
override def receive: Receive = {
case interestedIn: InterestedIn =>
registerInterest(interestedIn)
// 主要接受者由次要接收者取代(如果有)
case noLongerInterestedIn: NoLongerInterestedIn =>
unregisterInterest(noLongerInterestedIn)
case message: Any =>
sendFor(message)
}
// 注册即加入 interestRegistry Map 中
def registerInterest(interestedIn: InterestedIn): Unit = {
val messageType = typeOfMessage(interestedIn.messageType)
if (!interestRegistry.contains(messageType)) {
interestRegistry(messageType) = sender
} else {
secondaryInterestRegistry(messageType) = sender
}
if (interestRegistry.size + secondaryInterestRegistry.size >= canStartAfterRegistered) {
println("registry exceed limit")
context.system.terminate()
}
}
// 查看消息是否注册及注册的 sender
def sendFor(message: Any): Unit = {
val messageType = typeOfMessage(currentMirror.reflect(message).symbol.toString)
if (interestRegistry.contains(messageType)) {
interestRegistry(messageType) forward message
} else {
dunnoInterested ! message
}
}
def typeOfMessage(rawMessageType: String): String = {
rawMessageType.replace('$', ' ').replace('.', ' ').split(' ').last.trim
}
var unregisterCount: Int = 0
// 取消注册
def unregisterInterest(noLongerInterestedIn: NoLongerInterestedIn): Unit = {
val messageType = typeOfMessage(noLongerInterestedIn.messageType)
if (interestRegistry.contains(messageType)) {
val wasInterested = interestRegistry(messageType)
if (wasInterested.compareTo(sender) == 0) {
if (secondaryInterestRegistry.contains(messageType)) {
val nowInterested = secondaryInterestRegistry.remove(messageType)
interestRegistry(messageType) = nowInterested.get
} else {
interestRegistry.remove(messageType)
}
unregisterCount = unregisterCount + 1
if (unregisterCount >= this.canCompleteAfterUnregistered) {
println("unregister count > can CompleteAfterUnregistered")
context.system.terminate()
}
}
}
}
}
// 各种业务 sender
package io.binglau.scala.akka.demo.dynamic_router
import akka.actor.{Actor, ActorRef}
class TypeAInterested(interestRouter: ActorRef) extends Actor {
interestRouter ! InterestedIn(TypeAMessage.getClass.getName)
override def receive: Receive = {
case message: TypeAMessage =>
println(s"TypeAInterested: received: $message")
case message: Any =>
println(s"TypeAInterested: received unexpected message: $message")
}
}
class TypeBInterested(interestRouter: ActorRef) extends Actor {
interestRouter ! InterestedIn(TypeBMessage.getClass.getName)
override def receive: Receive = {
case message: TypeBMessage =>
println(s"TypeBInterested: received: $message")
case message: Any =>
println(s"TypeBInterested: received unexpected message: $message")
}
}
class TypeCInterested(interestRouter: ActorRef) extends Actor {
interestRouter ! InterestedIn(TypeCMessage.getClass.getName)
override def receive: Receive = {
case message: TypeCMessage =>
println(s"TypeCInterested: received: $message")
interestRouter ! NoLongerInterestedIn(TypeCMessage.getClass.getName)
case message: Any =>
println(s"TypeCInterested: received unexpected message: $message")
}
}
class TypeCAlsoInterested(interestRouter: ActorRef) extends Actor {
interestRouter ! InterestedIn(TypeCMessage.getClass.getName)
def receive = {
case message: TypeCMessage =>
println(s"TypeCAlsoInterested: received: $message")
interestRouter ! NoLongerInterestedIn(TypeCMessage.getClass.getName)
case message: Any =>
println(s"TypeCAlsoInterested: received unexpected message: $message")
}
}
// 不感兴趣的消息
package io.binglau.scala.akka.demo.dynamic_router
import akka.actor.Actor
// 未注册消息
class DunnoInterested extends Actor {
override def receive: Receive = {
case message: Any =>
println(s"DunnoInterest: received undeliverable message: $message")
}
}
// 执行 demo
package io.binglau.scala.akka.demo.dynamic_router
import akka.actor.{ActorSystem, Props}
import scala.concurrent.Await
import scala.concurrent.duration._
object DynamicRouter extends App {
val system = ActorSystem("dynamicRouter")
val dunnoInterested = system.actorOf(Props[DunnoInterested], "dunnoInterested")
val canStartAfterRegistered: Int = 5
val canCompleteAfterUnregistered: Int = 1
val typedMessageInterestRouter =
system.actorOf(Props(
new TypedMessageInterestRouter(dunnoInterested, canStartAfterRegistered, canCompleteAfterUnregistered)),
"typedMessageInterestRouter")
val typeAInterest = system.actorOf(Props(classOf[TypeAInterested], typedMessageInterestRouter), "typeAInterest")
val typeBInterest = system.actorOf(Props(classOf[TypeBInterested], typedMessageInterestRouter), "typeBInterest")
val typeCInterest = system.actorOf(Props(classOf[TypeCInterested], typedMessageInterestRouter), "typeCInterest")
val typeCAlsoInterested = system.actorOf(Props(classOf[TypeCAlsoInterested], typedMessageInterestRouter), "typeCAlsoInterested")
typedMessageInterestRouter ! TypeAMessage("Message of TypeA.")
typedMessageInterestRouter ! TypeBMessage("Message of TypeB.")
typedMessageInterestRouter ! TypeCMessage("Message of TypeC.")
typedMessageInterestRouter ! TypeCMessage("Another message of TypeC.")
typedMessageInterestRouter ! TypeDMessage("Message of TypeD.")
println("DynamicRouter: is completed.")
Await.ready(system.whenTerminated, 5.seconds)
}
/**
[DEBUG] [09/19/2017 00:37:03.655] [main] [EventStream] StandardOutLogger started
[DEBUG] [09/19/2017 00:37:04.361] [main] [EventStream(akka://dynamicRouter)] logger log1-Logging$DefaultLogger started
[DEBUG] [09/19/2017 00:37:04.361] [main] [EventStream(akka://dynamicRouter)] logger log1-Logging$DefaultLogger started
[DEBUG] [09/19/2017 00:37:04.364] [main] [EventStream(akka://dynamicRouter)] Default Loggers started
[DEBUG] [09/19/2017 00:37:04.364] [main] [EventStream(akka://dynamicRouter)] Default Loggers started
DynamicRouter: is completed.
TypeAInterested: received: TypeAMessage(Message of TypeA.)
TypeBInterested: received: TypeBMessage(Message of TypeB.)
TypeCInterested: received: TypeCMessage(Message of TypeC.)
TypeCInterested: received: TypeCMessage(Another message of TypeC.)
DunnoInterest: received undeliverable message: TypeDMessage(Message of TypeD.)
unregister count > can CompleteAfterUnregistered
[DEBUG] [09/19/2017 00:37:05.974] [dynamicRouter-akka.actor.default-dispatcher-3] [EventStream] shutting down: StandardOutLogger started
[DEBUG] [09/19/2017 00:37:05.974] [dynamicRouter-akka.actor.default-dispatcher-3] [EventStream] shutting down: StandardOutLogger started
[DEBUG] [09/19/2017 00:37:05.981] [dynamicRouter-akka.actor.default-dispatcher-3] [EventStream] all default loggers stopped
**/
分散-聚集路由器
分离器
当需要将较大的消息分割成多个独立部分,并将这些独立部分作为消息发送时,可使用分离器。分离器主要用于将构成一条消息的各个独立部分,分发给不同的子系统。
聚合器
将一些消息整合作为整体发送。
package io.binglau.scala.akka.demo.scatter_gather
import java.util.concurrent.TimeUnit
import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
import akka.actor._
case class RequestForQuotation(rfqId: String, retailItems: Seq[RetailItem]) {
val totalRetailPrice: Double = retailItems.map(retailItem => retailItem.retailPrice).sum
}
case class RetailItem(itemId: String, retailPrice: Double)
case class RequestPriceQuote(rfqId: String, itemId: String, retailPrice: Double, orderTotalRetailPrice: Double)
case class PriceQuote(quoterId: String, rfqId: String, itemId: String, retailPrice: Double, discountPrice: Double)
case class PriceQuoteFulfilled(priceQuote: PriceQuote)
case class PriceQuoteTimedOut(rfqId: String)
case class RequiredPriceQuotesForFulfillment(rfqId: String, quotesRequested: Int)
case class QuotationFulfillment(rfqId: String, quotesRequested: Int, priceQuotes: Seq[PriceQuote], requester: ActorRef)
case class BestPriceQuotation(rfqId: String, priceQuotes: Seq[PriceQuote])
case class SubscribeToPriceQuoteRequests(quoterId: String, quoteProcessor: ActorRef)
// 使用发布-订阅,将消息发送给已注册的接收者
object ScatterGather extends App {
val system = ActorSystem("demo")
val priceQuoteAggregator = system.actorOf(Props[PriceQuoteAggregator], "priceQuoteAggregator")
val orderProcessor = system.actorOf(Props(classOf[MountaineeringSuppliesOrderProcessor], priceQuoteAggregator), "orderProcessor")
// 对于报价不同的业务处理
system.actorOf(Props(classOf[BudgetHikersPriceQuotes], orderProcessor), "budgetHikers")
system.actorOf(Props(classOf[HighSierraPriceQuotes], orderProcessor), "highSierra")
system.actorOf(Props(classOf[MountainAscentPriceQuotes], orderProcessor), "mountainAscent")
system.actorOf(Props(classOf[PinnacleGearPriceQuotes], orderProcessor), "pinnacleGear")
system.actorOf(Props(classOf[RockBottomOuterwearPriceQuotes], orderProcessor), "rockBottomOuterwear")
orderProcessor ! RequestForQuotation("123",
Vector(RetailItem("1", 29.95),
RetailItem("2", 99.95),
RetailItem("3", 14.95)))
orderProcessor ! RequestForQuotation("125",
Vector(RetailItem("4", 39.99),
RetailItem("5", 199.95),
RetailItem("6", 149.95),
RetailItem("7", 724.99)))
orderProcessor ! RequestForQuotation("129",
Vector(RetailItem("8", 119.99),
RetailItem("9", 499.95),
RetailItem("10", 519.00),
RetailItem("11", 209.50)))
orderProcessor ! RequestForQuotation("135",
Vector(RetailItem("12", 0.97),
RetailItem("13", 9.50),
RetailItem("14", 1.99)))
orderProcessor ! RequestForQuotation("140",
Vector(RetailItem("15", 1295.50),
RetailItem("16", 9.50),
RetailItem("17", 599.99),
RetailItem("18", 249.95),
RetailItem("19", 789.99)))
println("Scatter-Gather: is completed.")
Await.ready(system.whenTerminated, 5.seconds)
}
class MountaineeringSuppliesOrderProcessor(priceQuoteAggregator: ActorRef) extends Actor {
val subscribers: scala.collection.mutable.Map[String, SubscribeToPriceQuoteRequests] =
scala.collection.mutable.Map[String, SubscribeToPriceQuoteRequests]()
// 将 RequestPriceQuote 消息发送给所有订阅者
def dispatch(rfq: RequestForQuotation): Unit = {
subscribers.values.foreach { subscriber =>
val quoteProcessor = subscriber.quoteProcessor
rfq.retailItems.foreach { retailItem =>
println("OrderProcessor: " + rfq.rfqId + " item: " + retailItem.itemId + " to: " + subscriber.quoterId)
quoteProcessor ! RequestPriceQuote(rfq.rfqId, retailItem.itemId, retailItem.retailPrice, rfq.totalRetailPrice)
}
}
}
override def receive: Receive = {
case subscriber: SubscribeToPriceQuoteRequests =>
subscribers(subscriber.quoterId) = subscriber
case priceQuote: PriceQuote =>
priceQuoteAggregator ! PriceQuoteFulfilled(priceQuote)
println(s"OrderProcessor: received: $priceQuote")
case rfq: RequestForQuotation =>
priceQuoteAggregator ! RequiredPriceQuotesForFulfillment(rfq.rfqId, subscribers.size * rfq.retailItems.size)
dispatch(rfq)
case bestPriceQuotation: BestPriceQuotation =>
println(s"OrderProcessor: received: $bestPriceQuotation")
// 产生最佳报价,结束
context.system.terminate()
case message: Any =>
println(s"OrderProcessor: received unexpected message: $message")
}
}
class PriceQuoteAggregator extends Actor {
val fulfilledPriceQuotes: scala.collection.mutable.Map[String, QuotationFulfillment] =
scala.collection.mutable.Map[String, QuotationFulfillment]()
// BestPriceQuotation 消息包含通过多个报价引擎提供的 PriceQuote 实例合并出的最佳报价
def bestPriceQuotationFrom(quotationFulfillment: QuotationFulfillment): BestPriceQuotation = {
val bestPrices = scala.collection.mutable.Map[String, PriceQuote]()
quotationFulfillment.priceQuotes.foreach { priceQuote =>
if (bestPrices.contains(priceQuote.itemId)) {
if (bestPrices(priceQuote.itemId).discountPrice > priceQuote.discountPrice) {
bestPrices(priceQuote.itemId) = priceQuote
}
} else {
bestPrices(priceQuote.itemId) = priceQuote
}
}
BestPriceQuotation(quotationFulfillment.rfqId, bestPrices.values.toVector)
}
override def receive: Receive = {
case required: RequiredPriceQuotesForFulfillment =>
fulfilledPriceQuotes(required.rfqId) = QuotationFulfillment(required.rfqId, required.quotesRequested, Vector(), sender)
// 超时设置
// scheduler 将会在 duration 后将 message 送给 self
val duration = Duration.create(2, TimeUnit.SECONDS)
context.system.scheduler.scheduleOnce(duration, self, PriceQuoteTimedOut(required.rfqId))
case priceQuoteFulfilled: PriceQuoteFulfilled =>
priceQuoteRequestFulfilled(priceQuoteFulfilled)
println(s"PriceQuoteAggregator: fulfilled price quote: $PriceQuoteFulfilled")
case priceQuoteTimedOut: PriceQuoteTimedOut => // 报价超时设置
priceQuoteRequestTimedOut(priceQuoteTimedOut.rfqId)
case message: Any =>
println(s"PriceQuoteAggregator: received unexpected message: $message")
}
def priceQuoteRequestFulfilled(priceQuoteFulfilled: PriceQuoteFulfilled): Unit = {
if (fulfilledPriceQuotes.contains(priceQuoteFulfilled.priceQuote.rfqId)) {
val previousFulfillment = fulfilledPriceQuotes(priceQuoteFulfilled.priceQuote.rfqId)
val currentPriceQuotes = previousFulfillment.priceQuotes :+ priceQuoteFulfilled.priceQuote
val currentFulfillment =
QuotationFulfillment(
previousFulfillment.rfqId,
previousFulfillment.quotesRequested,
currentPriceQuotes,
previousFulfillment.requester)
if (currentPriceQuotes.size >= currentFulfillment.quotesRequested) {
quoteBestPrice(currentFulfillment)
} else {
fulfilledPriceQuotes(priceQuoteFulfilled.priceQuote.rfqId) = currentFulfillment
}
}
}
// 出现超时情况,PriceQuoteAggregator 会根据它收到的 PriceQuoteFulfilled 消息
// 的数量,为 MountaineeringSuppliesOrderProcessor 对象提供一条 BestPriceQuotation 消息
def priceQuoteRequestTimedOut(rfqId: String): Unit = {
if (fulfilledPriceQuotes.contains(rfqId)) {
quoteBestPrice(fulfilledPriceQuotes(rfqId))
}
}
def quoteBestPrice(quotationFulfillment: QuotationFulfillment): Unit = {
if (fulfilledPriceQuotes.contains(quotationFulfillment.rfqId)) {
quotationFulfillment.requester ! bestPriceQuotationFrom(quotationFulfillment)
fulfilledPriceQuotes.remove(quotationFulfillment.rfqId)
}
}
}
class BudgetHikersPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor {
val quoterId: String = self.path.name
// 注册
priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self)
override def receive: Receive = {
case rpq: RequestPriceQuote =>
if (rpq.orderTotalRetailPrice < 1000.00) {
val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice
sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount)
} else {
println(s"BudgetHikersPriceQuotes: ignoring: $rpq")
}
case message: Any =>
println(s"BudgetHikersPriceQuotes: received unexpected message: $message")
}
def discountPercentage(orderTotalRetailPrice: Double): Double = {
if (orderTotalRetailPrice <= 100.00) 0.02
else if (orderTotalRetailPrice <= 399.99) 0.03
else if (orderTotalRetailPrice <= 499.99) 0.05
else if (orderTotalRetailPrice <= 799.99) 0.07
else 0.075
}
}
class HighSierraPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor {
val quoterId: String = self.path.name
priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self)
override def receive: Receive = {
case rpq: RequestPriceQuote =>
val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice
sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount)
case message: Any =>
println(s"HighSierraPriceQuotes: received unexpected message: $message")
}
def discountPercentage(orderTotalRetailPrice: Double): Double = {
if (orderTotalRetailPrice <= 150.00) 0.015
else if (orderTotalRetailPrice <= 499.99) 0.02
else if (orderTotalRetailPrice <= 999.99) 0.03
else if (orderTotalRetailPrice <= 4999.99) 0.04
else 0.05
}
}
class MountainAscentPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor {
val quoterId: String = self.path.name
priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self)
override def receive: Receive = {
case rpq: RequestPriceQuote =>
val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice
sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount)
case message: Any =>
println(s"MountainAscentPriceQuotes: received unexpected message: $message")
}
def discountPercentage(orderTotalRetailPrice: Double): Double = {
if (orderTotalRetailPrice <= 99.99) 0.01
else if (orderTotalRetailPrice <= 199.99) 0.02
else if (orderTotalRetailPrice <= 499.99) 0.03
else if (orderTotalRetailPrice <= 799.99) 0.04
else if (orderTotalRetailPrice <= 999.99) 0.045
else if (orderTotalRetailPrice <= 2999.99) 0.0475
else 0.05
}
}
class PinnacleGearPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor {
val quoterId: String = self.path.name
priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self)
override def receive: Receive = {
case rpq: RequestPriceQuote =>
val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice
sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount)
case message: Any =>
println(s"PinnacleGearPriceQuotes: received unexpected message: $message")
}
def discountPercentage(orderTotalRetailPrice: Double): Double = {
if (orderTotalRetailPrice <= 299.99) 0.015
else if (orderTotalRetailPrice <= 399.99) 0.0175
else if (orderTotalRetailPrice <= 499.99) 0.02
else if (orderTotalRetailPrice <= 999.99) 0.03
else if (orderTotalRetailPrice <= 1199.99) 0.035
else if (orderTotalRetailPrice <= 4999.99) 0.04
else if (orderTotalRetailPrice <= 7999.99) 0.05
else 0.06
}
}
class RockBottomOuterwearPriceQuotes(priceQuoteRequestPublisher: ActorRef) extends Actor {
val quoterId: String = self.path.name
priceQuoteRequestPublisher ! SubscribeToPriceQuoteRequests(quoterId, self)
override def receive: Receive = {
case rpq: RequestPriceQuote =>
if (rpq.orderTotalRetailPrice < 2000.00) {
val discount = discountPercentage(rpq.orderTotalRetailPrice) * rpq.retailPrice
sender ! PriceQuote(quoterId, rpq.rfqId, rpq.itemId, rpq.retailPrice, rpq.retailPrice - discount)
} else {
println(s"RockBottomOuterwearPriceQuotes: ignoring: $rpq")
}
case message: Any =>
println(s"RockBottomOuterwearPriceQuotes: received unexpected message: $message")
}
def discountPercentage(orderTotalRetailPrice: Double): Double = {
if (orderTotalRetailPrice <= 100.00) 0.015
else if (orderTotalRetailPrice <= 399.99) 0.02
else if (orderTotalRetailPrice <= 499.99) 0.03
else if (orderTotalRetailPrice <= 799.99) 0.04
else if (orderTotalRetailPrice <= 999.99) 0.05
else if (orderTotalRetailPrice <= 2999.99) 0.06
else if (orderTotalRetailPrice <= 4999.99) 0.07
else if (orderTotalRetailPrice <= 5999.99) 0.075
else 0.08
}
}
轮询消费者
在这种模式中,消费者通过轮询方式向指定资源请求获取信息。在资源能够提供该信息前,需要阻塞消费者。与此相反,在使用 Actor 模型时,无法使 Actor 对象以轮询方式向另一个 Actor 对象请求信息,因为 Actor 对象之间的协同操作不会被阻塞。一个 Actor 对象从另一个 Actor 对象获取信息的唯一方式是使用请求—回复模式。也就是说,需要使用请求—回复模式高效地模拟轮询消费者模式。
在典型的过程化轮询环境中,工作消费者会从工作提供者那里请求获取工作。在工作提供者能够将被请求的工作分配给工作消费者前,工作消费者会被阻塞。Actor 模型中肯定不会出现这种情况。在使用 Actor 对象前,当工作消费者告诉工作提供者它需要获得工作时,工作消费者仍旧会在它本身的线程中继续运行,而且被发送给工作提供者的请求,过一段时间(这段时间可能长一点也可能短一点)才会被收到。不论接收和处理请求所需实际时间有多久,过程化轮询模式要求的在分配和回复被请求的工作时,使用的阻塞操作都无法被实现。
package io.binglau.scala.akka.demo.polling_consumer
import scala.collection.immutable.List
import akka.actor._
import scala.concurrent.duration._
import scala.concurrent.Await
object PollingConsumerDriver extends App {
val system = ActorSystem("demo")
val workItemsProvider: ActorRef = system.actorOf(Props[WorkItemsProvider], "workItemsProvider")
val workConsumer: ActorRef = system.actorOf(Props(classOf[WorkConsumer], workItemsProvider), "workConsumer")
workConsumer ! WorkNeeded()
println("PollingConsumerDriver: completed.")
Await.ready(system.whenTerminated, 5.seconds)
}
case class WorkNeeded()
case class WorkOnItem(workItem: WorkItem)
class WorkConsumer(workItemsProvider: ActorRef) extends Actor {
var totalItemsWorkedOn = 0
def performWorkOn(workItem: WorkItem): Unit = {
// 总工作数限制
totalItemsWorkedOn = totalItemsWorkedOn + 1
if (totalItemsWorkedOn >= 15) {
context.stop(self)
context.system.terminate()
}
}
override def postStop(): Unit = {
context.stop(workItemsProvider)
}
override def receive: Receive = {
case allocated: WorkItemsAllocated =>
// 获取资源,消耗资源,再发送申请资源请求
println("WorkItemsAllocated...")
allocated.workItems map { workItem =>
self ! WorkOnItem(workItem)
}
self ! WorkNeeded()
// 轮询起点,申请一次轮询数
case workNeeded: WorkNeeded =>
println("WorkNeeded...")
workItemsProvider ! AllocateWorkItems(5)
// 处理资源
case workOnItem: WorkOnItem =>
println(s"Performed work on: ${workOnItem.workItem.name}")
performWorkOn(workOnItem.workItem)
}
}
case class AllocateWorkItems(numberOfItems: Int)
case class WorkItemsAllocated(workItems: List[WorkItem])
case class WorkItem(name: String)
class WorkItemsProvider extends Actor {
var workItemsNamed: Int = 0
def allocateWorkItems(numberOfItems: Int): List[WorkItem] = {
var allocatedWorkItems = List[WorkItem]()
for (itemCount <- 1 to numberOfItems) {
val nameIndex = workItemsNamed + itemCount
allocatedWorkItems = allocatedWorkItems :+ WorkItem("WorkItem" + nameIndex)
}
workItemsNamed = workItemsNamed + numberOfItems
allocatedWorkItems
}
override def receive: Receive = {
case request: AllocateWorkItems =>
// 发送轮询资源
sender ! WorkItemsAllocated(allocateWorkItems(request.numberOfItems))
}
}
最后
总体上来说,我们一开始接触使用到 Akka 的目的或多或少都是因为其简单易用的并发开发方式,就是说,Akka 虽然说我可以利用它来模拟我们正在开发的任何应用,但是它本质上还是为了解决并发场景下编程的难度问题的,所以其最适合的场景,自然也是并发下的场景。
使用场景
-
事务处理(Transaction Processing)
在线游戏系统、金融/银行系统、交易系统、投注系统、社交媒体系统、电信服务系统。
-
并行计算(Concurrency/Parallelism)
任何具有并发/并行计算需求的行业,基于JVM的应用都可以使用,如使用编程语言 Scala、Java、Groovy、JRuby 开发。
-
复杂事件流处理(Complex Event Stream Processing)
Akka 本身提供的 Actor 就适合处理基于事件驱动的应用,所以可以更加容易处理具有复杂事件流的应用。
-
后端服务(Service Backend)
任何行业的任何类型的应用都可以使用,比如提供 REST、SOAP 等风格的服务,类似于一个服务总线,Akka 支持纵向 & 横向扩展,以及容错/高可用(HA)的特性。
番外
另外随便说一下,之前在搜集资料的时候,看到了这样的一篇文章
Using Scala and Akka with Domain-Driven Design
所谓 DDD 领域驱动设计(DDD:Domain-Driven Design)。维基百科是有着这样的解释:
Domain-driven design (DDD) is an approach to software development for complex needs by connecting the implementation to an evolving model.[1] The premise of domain-driven design is the following:
- placing the project's primary focus on the core domain and domain logic;(关注项目中涉及到的核心领域以及其领域逻辑)
- basing complex designs on a model of the domain;(基于领域模型上进行复杂的架构)
- initiating a creative collaboration between technical and domain experts to iteratively refine a conceptual model that addresses particular domain problems.(易于开启技术与领域专家之间的合作,以迭代的方式改进在特定领域下的特定问题的解决模型)
虽说设计与语言无关,但是在 Akka 编程的时候有特别大的优势是,Actor 模型限制了你,在一个 Actor 中你只能获取有限的数据做有限的事,很好的将你所要完成的事情分为一个个单独的模块,而且一个 Actor 获知其他 Actor 的方式及其有限,只要不是瞎写(不受限制的创建子 Actor 会内存泄露)基本是模块分明,在加上路由器的设置,可以将中心领域问题凸显出来。
参考文档
Akka 官方文档
《响应式架构——消息模式 Actor 实现与 Scala、Akka 应用集成》
Akka 基础功能
Akka 基础功能
生命周期
package io.binglau.scala.akka.demo
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, ActorSystem, OneForOneStrategy, Props, SupervisorStrategy}
import akka.event.Logging
import akka.pattern.{Backoff, BackoffSupervisor}
import scala.concurrent.duration.Duration
class LifeCycle extends Actor{
val log = Logging(context.system, this)
// Actor 对象被创建后启动前该方法会被调用
// Actor 对象是通过异步方式创建的
override def preStart(): Unit = {
log.info("preStart")
}
// Actor 对象停止运行时,该方法会被调用
// 当 ActorSystem 或 ActorContext 对象(Actor 对象中会
// 含有一个 ActorContext 对象,可通过 context 方法获得)中的
// stop(ActorRef) 方法被调用是,Actor对象会以一部方式停止运行.
override def postStop(): Unit = {
log.info("postStop")
}
// 通过失效监督策略可以重启失效的子 Actor 对象。在
// 执行重启操作的过程中,可以在重启 Actor 对象前调用该方法。
// 该方法默认的实现代码会处理 Actor 对象停止运行前使用的资源,
// 处理 Actor 对象的所有子对象。执行完清理操作后,postStop() 方法会自动被调用。
// 通常不必重写
override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
log.info(s"preRestart e: ${reason.getMessage}, message: ${message.toString}")
super.preRestart(reason, message)
}
// Actor 对象重启后回调用这个方法,使 Actor 对象能够在失效
// 并重启后被初始化。该方法默认的执行代码会调用 preStart() 方法。
// 通常不必重写
override def postRestart(reason: Throwable): Unit = {
log.info(s"postRestart e: ${reason.getMessage}")
super.postRestart(reason)
}
override def receive: Receive = {
case "Restart" =>
log.info("Restart")
throw new NullPointerException
case _ => log.info("receive something")
}
}
object LifeCycleDemo {
def main(args: Array[String]): Unit = {
val system = ActorSystem("demo")
val lifeCycle = Props(classOf[LifeCycle])
val supervisor = BackoffSupervisor.props(
Backoff.onStop(
lifeCycle,
childName = "myDemo",
minBackoff = Duration.create(3, TimeUnit.SECONDS),
maxBackoff = Duration.create(30, TimeUnit.SECONDS),
randomFactor = 0.2 // adds 20% "noise" to vary the intervals slightly
).withSupervisorStrategy(
OneForOneStrategy() {
case e: NullPointerException => SupervisorStrategy.Restart
case _ => SupervisorStrategy.Escalate
}
))
val actor = system.actorOf(lifeCycle, "lifeCycle")
system.actorOf(supervisor, "lifeCycleSupervisor")
actor ! "Test"
actor ! "Restart"
Thread.sleep(1000)
actor ! "Test"
system.terminate()
}
}
/**
[INFO] [09/12/2017 00:36:53.376] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/lifeCycle] preStart
[INFO] [09/12/2017 00:36:53.376] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/lifeCycle] receive something
[INFO] [09/12/2017 00:36:53.376] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/lifeCycle] Restart
[INFO] [09/12/2017 00:36:53.377] [demo-akka.actor.default-dispatcher-2] [akka://demo/user/lifeCycleSupervisor/myDemo] preStart
[ERROR] [09/12/2017 00:36:53.398] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/lifeCycle] null
java.lang.NullPointerException
at io.binglau.scala.akka.demo.LifeCycle$$anonfun$receive$1.applyOrElse(LifeCycle.scala:49)
at akka.actor.Actor.aroundReceive(Actor.scala:513)
at akka.actor.Actor.aroundReceive$(Actor.scala:511)
at io.binglau.scala.akka.demo.LifeCycle.aroundReceive(LifeCycle.scala:11)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:527)
at akka.actor.ActorCell.invoke(ActorCell.scala:496)
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
at akka.dispatch.Mailbox.run(Mailbox.scala:224)
at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
[INFO] [09/12/2017 00:36:53.410] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/lifeCycle] preRestart e: null, message: Some(Restart)
[INFO] [09/12/2017 00:36:53.411] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/lifeCycle] postStop
[INFO] [09/12/2017 00:36:53.413] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/lifeCycle] postRestart e: null
[INFO] [09/12/2017 00:36:53.413] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/lifeCycle] preStart
[INFO] [09/12/2017 00:36:54.380] [demo-akka.actor.default-dispatcher-4] [akka://demo/user/lifeCycle] receive something
[INFO] [09/12/2017 00:36:54.419] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/lifeCycle] postStop
[INFO] [09/12/2017 00:36:54.426] [demo-akka.actor.default-dispatcher-2] [akka://demo/user/lifeCycleSupervisor/myDemo] postStop
Process finished with exit code 0
**/
更改状态
package io.binglau.scala.akka.demo
import akka.actor.{Actor, ActorSystem, Props}
import akka.event.Logging
// become(...) 使用该函数可以通过指定偏函数设置 Actor 对象的当前行为。
// 该函数有两个参数,第二个参数为 discardOld, 默认为 true。当该参数为 true 时,
// 在设置当前行为前,Actor 对象的上个行为会被丢弃。当该参数为 false 时,
// Actor 对象的上一个行为仍旧被保存在堆栈中,当前设置的 Actor 对象行为也会被推入
// 堆栈,并位于上一个行为条目的上方
// unbecome 在上一个行为存在的情况下,使用该函数可以使 Actor 对象从当前行为
// 切换到上一个行为
class StatusSwap extends Actor{
import context._
val log = Logging(context.system, this)
def angry: Receive = {
case "foo" => log.info("I am already angry?")
case "bar" =>
become(happy)
log.info("angry become happy")
case "return" =>
unbecome()
log.info("angry unbecome")
}
def happy: Receive = {
case "bar" => log.info("I am already happy :-)")
case "foo" =>
become(angry, false)
// become(angry)
log.info("happy become angry")
case "return" =>
unbecome()
log.info("happy unbecome")
}
override def receive: Receive = {
case "foo" =>
log.info("become angry")
become(angry)
case "bar" =>
log.info("become happy")
become(happy)
}
}
// The App trait can be used to quickly turn objects into executable programs.
// Here, object Main inherits the main method of App.
// args returns the current command line arguments as an array.
object StatusDemo extends App {
// Console.println("Hello World: " + (args mkString ", "))
val system = ActorSystem("demo")
val statusSwap = system.actorOf(Props(classOf[StatusSwap]), "statusSwap")
statusSwap ! "bar" // happy
statusSwap ! "bar"
statusSwap ! "foo" // angry
statusSwap ! "foo"
statusSwap ! "return" // happy
statusSwap ! "bar"
system.terminate()
}
/**
[INFO] [09/13/2017 23:23:03.776] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] become happy
[INFO] [09/13/2017 23:23:03.777] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] I am already happy :-)
[INFO] [09/13/2017 23:23:03.778] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] happy become angry
[INFO] [09/13/2017 23:23:03.778] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] I am already angry?
[INFO] [09/13/2017 23:23:03.778] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] angry unbecome
[INFO] [09/13/2017 23:23:03.778] [demo-akka.actor.default-dispatcher-5] [akka://demo/user/statusSwap] I am already happy :-)
**/
监督
任何创建了子 Actor 对象的 Actor 对象,都会自动变为其子 Actor 对象的监督者。如果子 Actor 对象崩溃了(例如抛出了异常),那么它的父 Actor 对象就必须在执行下列操作之间做出选择:
- 使子对象继续运行
- 重启子对象
- 停止子对象
- 使失效情况升级(将控制权移交给祖父对象)
package io.binglau.scala.akka.demo
import akka.actor.SupervisorStrategy.{Escalate, Restart, Resume, Stop}
import akka.actor.{Actor, ActorRef, ActorSystem, OneForOneStrategy, Props}
import akka.event.Logging
import scala.concurrent.duration.DurationInt
class SupervisorChild extends Actor {
val log = Logging(context.system, this)
override def receive: Receive = {
case "null" => throw new NullPointerException
case "arith" => throw new ArithmeticException
case "illegal" => throw new IllegalArgumentException
case "unsupport" => throw new UnsupportedOperationException
case "exception" => throw new Exception
case _ => log.info("unknow")
}
}
class Supervisor extends Actor {
import context._
val log = Logging(context.system, this)
var child: ActorRef = Actor.noSender
// 实例化一个 SupervisorStrategy 子类 (AllForOneStrategy / OneForOneStrategy)。
// 通常只是声明 OneForOneStrategy 子类,因为该策略仅会应用于崩溃的子 Actor 对象。
// 使用 AllForOneStrategy 子类的情况比较少,因为它会对所有子 Actor 对象产生 Decider 偏
// 函数效果(将失效情况与处理手段对应起来),而不是仅对崩溃的子 Actor 对象生效
override val supervisorStrategy =
OneForOneStrategy(
maxNrOfRetries = 5, // 运行 Actor 对象崩溃的次数, -1 为次数不限
withinTimeRange = 1 minute // 放弃 Actor 对象前重新启动该对象的时限
) { // Decider 偏函数
case _: NullPointerException => Restart // 重启
case _: ArithmeticException => Resume // 保持原状态继续运行
case _: IllegalArgumentException => Stop // 停止
case _: UnsupportedOperationException => Stop // 停止
case _: Exception => Escalate // 失效,处理权移交给祖父对象
}
def input: Receive = {
case msg: String => child ! msg
case _ => log.info("unknow")
}
override def receive: Receive = {
case "A" =>
child = context.actorOf(Props[SupervisorChild], "childA")
become(input)
case _ =>
throw new NullPointerException
}
}
object SupervisorDemo extends App {
val system = ActorSystem("demo")
val supervisor = system.actorOf(Props(classOf[Supervisor]), "supervisor")
supervisor ! "A"
supervisor ! "abc"
supervisor ! "arith"
supervisor ! "null"
Thread.sleep(1000)
supervisor ! "abc"
supervisor ! "illegal"
supervisor ! "abc"
}
/**
[INFO] [09/14/2017 00:20:28.573] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/supervisor/childA] unknow
[WARN] [09/14/2017 00:20:28.581] [demo-akka.actor.default-dispatcher-4] [akka://demo/user/supervisor/childA] null
[ERROR] [09/14/2017 00:20:28.584] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/supervisor/childA] null
java.lang.NullPointerException
at io.binglau.scala.akka.demo.SupervisorChild$$anonfun$receive$1.applyOrElse(Supervisor.scala:12)
at akka.actor.Actor.aroundReceive(Actor.scala:513)
at akka.actor.Actor.aroundReceive$(Actor.scala:511)
at io.binglau.scala.akka.demo.SupervisorChild.aroundReceive(Supervisor.scala:9)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:527)
at akka.actor.ActorCell.invoke(ActorCell.scala:496)
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
at akka.dispatch.Mailbox.run(Mailbox.scala:224)
at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
[INFO] [09/14/2017 00:20:29.572] [demo-akka.actor.default-dispatcher-4] [akka://demo/user/supervisor/childA] unknow
[ERROR] [09/14/2017 00:20:29.573] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/supervisor/childA] null
java.lang.IllegalArgumentException
at io.binglau.scala.akka.demo.SupervisorChild$$anonfun$receive$1.applyOrElse(Supervisor.scala:14)
at akka.actor.Actor.aroundReceive(Actor.scala:513)
at akka.actor.Actor.aroundReceive$(Actor.scala:511)
at io.binglau.scala.akka.demo.SupervisorChild.aroundReceive(Supervisor.scala:9)
at akka.actor.ActorCell.receiveMessage(ActorCell.scala:527)
at akka.actor.ActorCell.invoke(ActorCell.scala:496)
at akka.dispatch.Mailbox.processMailbox(Mailbox.scala:257)
at akka.dispatch.Mailbox.run(Mailbox.scala:224)
at akka.dispatch.Mailbox.exec(Mailbox.scala:234)
at akka.dispatch.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at akka.dispatch.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at akka.dispatch.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at akka.dispatch.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
[INFO] [09/14/2017 00:20:29.579] [demo-akka.actor.default-dispatcher-3] [akka://demo/user/supervisor/childA] Message [java.lang.String] from Actor[akka://demo/user/supervisor#347975802] to Actor[akka://demo/user/supervisor/childA#794249311] was not delivered. [1] dead letters encountered. This logging can be turned off or adjusted with configuration settings 'akka.log-dead-letters' and 'akka.log-dead-letters-during-shutdown'.
[INFO] [09/14/2017 00:20:53.607] [Thread-0] [CoordinatedShutdown(akka://demo)] Starting coordinated shutdown from JVM shutdown hook
**/
层级结构
当 Actor 系统被创建时,有几个 Actor 对象会随着它一起被创建。其中包括:
- root 守护对象
- user 守护对象(应用程序创建的 Actor 对象上方)
- system 守护对象
// will look up this absolute path
context.actorSelection("/user/serviceA/aggregator")
// will look up sibling beneath same supervisor
context.actorSelection("../joe")
// remote
context.actorSelection("akka.tcp://app@otherhost:1234/user/serviceB")
订阅发布
三个分类器:
-
LookupClassification:通过匹配指定的事件类型,支持简单的查询操作。
// must define a full order over the subscribers, expressed as expected from // `java.lang.Comparable.compare` override protected def compareSubscribers(a: Subscriber, b: Subscriber): Int = a.compareTo(b)
-
SubchannelClassification:通过匹配事件的类型和子类型,支持子通道层次结构
-
ScanningClassification:在某个事件落入两个或多个通道,并且需要将该事件发布给所有合法订阅者,因而需要扫描 EventBus 类的全部分类时,应混入该特征。
// is needed for determining matching classifiers and storing them in an // ordered collection override protected def compareClassifiers(a: Classifier, b: Classifier): Int = if (a < b) -1 else if (a == b) 0 else 1 // is needed for storing subscribers in an ordered collection override protected def compareSubscribers(a: Subscriber, b: Subscriber): Int = a.compareTo(b) // determines whether a given classifier shall match a given event; it is invoked // for each subscription for all received events, hence the name of the classifier override protected def matches(classifier: Classifier, event: Event): Boolean = event.length <= classifier
package io.binglau.scala.akka.demo
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.event.{EventBus, SubchannelClassification}
import akka.util.Subclassification
final case class MsgEnvelope(topic: String, payload: Any)
class StartsWithSubClassification extends Subclassification[String] {
override def isEqual(x: String, y: String): Boolean = x == y
override def isSubclass(x: String, y: String): Boolean = x.startsWith(y)
}
class SubChannel extends EventBus with SubchannelClassification {
// 需要处理的订阅事件
override type Event = MsgEnvelope
// 分类器
override type Classifier = String
// 发布器
override type Subscriber = ActorRef
// 提供两种方法 isEqual 和 isSubClass,将分类器作为一个参数 x 与 classify 返回的值作为另一个参数传入,
// 两个方法返回任意 true 则可进入
override protected implicit def subclassification: Subclassification[Classifier] = new StartsWithSubClassification
// 订阅事件哪部分作为分类信息
override protected def classify(event: Event): Classifier = event.topic
// 分类之后的发布操作
override protected def publish(event: Event, subscriber: Subscriber): Unit = {
subscriber ! event.payload
}
}
object SubchannelDemo {
def main(args: Array[String]): Unit = {
val system = ActorSystem("Test")
val print = system.actorOf(Props(classOf[Print]), "print")
val subchannelBus = new SubChannel
// 设置发布器和分类器
subchannelBus.subscribe(print, "abc")
subchannelBus.publish(MsgEnvelope("xyzabc", "x"))
subchannelBus.publish(MsgEnvelope("bcdef", "b"))
subchannelBus.publish(MsgEnvelope("abc", "c"))
subchannelBus.publish(MsgEnvelope("abcdef", "d"))
}
}
/**
[INFO] [09/14/2017 23:42:47.268] [Test-akka.actor.default-dispatcher-5] [akka://Test/user/print] receive c
[INFO] [09/14/2017 23:42:47.269] [Test-akka.actor.default-dispatcher-5] [akka://Test/user/print] receive d
**/
Dispatchers
添加配置在 /src/main/resources 中添加一份 application.conf
An Akka
MessageDispatcher
is what makes Akka Actors “tick”, it is the engine of the machine so to speak. AllMessageDispatcher
implementations are also anExecutionContext
, which means that they can be used to execute arbitrary code, for instance Futures.
通过下面代码可以得到一个配置的 Dispatcher
// for use with Futures, Scheduler, etc.
implicit val executionContext = system.dispatchers.lookup("my-dispatcher")
设置 Dispatcher
my-dispatcher {
# Dispatcher is the name of the event-based dispatcher
type = Dispatcher
# What kind of ExecutionService to use
executor = "fork-join-executor"
# Configuration for the fork join pool
fork-join-executor {
# Min number of threads to cap factor-based parallelism number to
parallelism-min = 2
# Parallelism (threads) ... ceil(available processors * factor)
parallelism-factor = 2.0
# Max number of threads to cap factor-based parallelism number to
parallelism-max = 10
}
# Throughput defines the maximum number of messages to be
# processed per actor before the thread jumps to the next actor.
# Set to 1 for as fair as possible.
throughput = 100
}
配置Actor使用一个特定的disptacher:
import akka.actor.Props
val myActor = context.actorOf(Props[MyActor], "myactor")
akka.actor.deployment {
/myactor {
dispatcher = my-dispatcher
}
}
或者在代码中设置
import akka.actor.Props
val myActor =
context.actorOf(Props[MyActor].withDispatcher("my-dispatcher"), "myactor1")
type
-
Dispatcher
This is an event-based dispatcher that binds a set of Actors to a thread pool. It is the default dispatcher used if one is not specified.
- 可共享性: 无限制
- 邮箱: 任何一种类型,为每一个Actor创建一个
- 使用场景: 默认派发器,Bulkheading
- 底层使用:
java.util.concurrent.ExecutorService
可以指定“executor”使用“fork-join-executor”, “thread-pool-executor” 或者 the FQCN(类名的全称) of an akka.dispatcher.ExecutorServiceConfigurator
-
PinnedDispatcher
This dispatcher dedicates a unique thread for each actor using it; i.e. each actor will have its own thread pool with only one thread in the pool.
- 可共享性: 无
- 邮箱: 任何一种类型,为每个Actor创建一个
- 使用场景: Bulkheading
- 底层使用: 任何 akka.dispatch.ThreadPoolExecutorConfigurator
缺省为一个 “thread-pool-executor”
-
CallingThreadDispatcher
This dispatcher runs invocations on the current thread only. This dispatcher does not create any new threads, but it can be used from different threads concurrently for the same actor. See CallingThreadDispatcher for details and restrictions.
- 可共享性: 无限制
- 邮箱: 任何一种类型,每Actor每线程创建一个(需要时)
- 使用场景: 测试
- 底层使用: 调用的线程 (duh)
确保送达机制
at-least-once-delivery
To send messages with at-least-once delivery semantics to destinations you can mix-in
AtLeastOnceDelivery
trait to yourPersistentActor
on the sending side. It takes care of re-sending messages when they have not been confirmed within a configurable timeout.结合持久化保证一定时间被确认(发送端)
The state of the sending actor, including which messages have been sent that have not been confirmed by the recipient must be persistent so that it can survive a crash of the sending actor or JVM. The
AtLeastOnceDelivery
trait does not persist anything by itself. It is your responsibility to persist the intent that a message is sent and that a confirmation has been received.AtLeastOnceDelivery 不会持久化任何东西,你需要去持久化试图发送的消息并处理确认信息
package io.binglau.scala.akka.demo
import akka.actor.{Actor, ActorSelection}
import akka.event.Logging
import akka.persistence.{AtLeastOnceDelivery, PersistentActor}
case class Msg(deliveryId: Long, s: String)
case class Confirm(deliveryId: Long)
// sealed
// 其修饰的trait,class只能在当前文件里面被继承
// 用sealed修饰这样做的目的是告诉scala编译器在检查模式匹配的时候,
// 让scala知道这些case的所有情况,scala就能够在编译的时候进行检查,
// 看你写的代码是否有没有漏掉什么没case到,减少编程的错误。
sealed trait Evt
case class MsgSent(s: String) extends Evt
case class MsgConfirmed(deliveryId: Long) extends Evt
class MyPersistentActor(destination: ActorSelection)
extends PersistentActor with AtLeastOnceDelivery {
// 在任何情况下,从主题 / 队列持久性 Actor 对象接收消息的持久性视图 Actor 实例,
// 都会将它的 persistenceId 作为相应主题 / 队列持久性 Actor 对象标识符
override def persistenceId: String = "persistence-id"
// 通过 receiveCommand 来接受新消息
override def receiveCommand: Receive = {
// To send messages to the destination path, use the **deliver** method after
// you have persisted the intent to send the message.
case s: String => persist(MsgSent(s))(updateState)
// The destination actor must send back a confirmation message.
// When the sending actor receives this confirmation message you should
// persist the fact that the message was delivered successfully
// and then call the confirmDelivery method.
case Confirm(deliveryId) => persist(MsgConfirmed(deliveryId))(updateState)
}
// 发送确认回执
override def receiveRecover: Receive = {
case evt: Evt => updateState(evt)
}
def updateState(evt: Evt): Unit = evt match {
// deliver自动产生一个deliveryId,这个deliveryId是发送方与接收方沟通的标志
case MsgSent(s) =>
deliver(destination)(deliveryId => Msg(deliveryId, s))
case MsgConfirmed(deliveryId) => confirmDelivery(deliveryId)
}
}
class MyDestination extends Actor {
val log = Logging(context.system, this)
def receive = {
case Msg(deliveryId, s) =>
log.info(s)
sender() ! Confirm(deliveryId)
}
}
也可参考 Stream 相关内容
应用Demo
请求—回复模式
package io.binglau.scala.akka.demo
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
case class Request(what: String)
case class Reply(what: String)
case class StartWith(server: ActorRef)
// 方式
// 1. 发送消息带上 server
// 2. 初始化带上 server
class Client extends Actor {
def receive = {
case StartWith(server) =>
println("Client: is starting...")
server ! Request("REQ-1")
case Reply(what) =>
println("Client: received response: " + what)
case _ =>
println("Client: received unexpected message")
}
}
/**
* 一个 Actor 对象可以通过下列方式获得其他 Actor 对象的地址
* 1. 一个 Actor 对象创建了另一个 Actor 对象
* 2. 一个 Actor 对象收到消息,包含其他 Actor 对象地址
* 3. 有时 Actor 对象可以根据名称(selection)查询 Actor,但是这么做会带来不合适的定义和实现束缚
*/
class Server extends Actor {
def receive = {
case Request(what) =>
println("Server: received request value: " + what)
sender ! Reply("RESP-1 for " + what)
case _ =>
println("Server: received unexpected message")
}
}
object RequestReplyDemo extends App {
val system = ActorSystem("demo")
val client = system.actorOf(Props[Client], "client")
val server = system.actorOf(Props[Server], "server")
client ! StartWith(server)
println("RequestReply: is completed.")
system.terminate()
}
管道与过滤器
package io.binglau.scala.akka.demo
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
case class ProcessIncomingOrder(orderInfo: Array[Byte])
class Authenticator(nextFilter: ActorRef) extends Actor {
def receive = {
case message: ProcessIncomingOrder =>
val text = new String(message.orderInfo)
println(s"Authenticator: processing $text")
val orderText = text.replace("(certificate)", "")
nextFilter ! ProcessIncomingOrder(orderText.toCharArray.map(_.toByte))
}
}
class Decrypter(nextFilter: ActorRef) extends Actor {
def receive = {
case message: ProcessIncomingOrder =>
val text = new String(message.orderInfo)
println(s"Decrypter: processing $text")
val orderText = text.replace("(encryption)", "")
nextFilter ! ProcessIncomingOrder(orderText.toCharArray.map(_.toByte))
}
}
class Deduplicator(nextFilter: ActorRef) extends Actor {
val processedOrderIds = scala.collection.mutable.Set[String]()
def orderIdFrom(orderText: String): String = {
val orderIdIndex = orderText.indexOf("id='") + 4
val orderIdLastIndex = orderText.indexOf("'", orderIdIndex)
orderText.substring(orderIdIndex, orderIdLastIndex)
}
def receive = {
case message: ProcessIncomingOrder =>
val text = new String(message.orderInfo)
println(s"Deduplicator: processing $text")
val orderId = orderIdFrom(text)
if (processedOrderIds.add(orderId)) {
nextFilter ! message
} else {
println(s"Deduplicator: found duplicate order $orderId")
}
}
}
class OrderAcceptanceEndpoint(nextFilter: ActorRef) extends Actor {
def receive = {
case rawOrder: Array[Byte] =>
val text = new String(rawOrder)
println(s"OrderAcceptanceEndpoint: processing $text")
nextFilter ! ProcessIncomingOrder(rawOrder)
}
}
class OrderManagementSystem extends Actor {
def receive = {
case message: ProcessIncomingOrder =>
val text = new String(message.orderInfo)
println(s"OrderManagementSystem: processing unique order: $text")
}
}
object PipesAndFilterDemo extends App{
val system = ActorSystem("demo")
val orderText = "(encryption)(certificate)<order id='123'>...</order>"
val rawOrderBytes = orderText.toCharArray.map(_.toByte)
// 链式过滤器
val filter5 = system.actorOf(Props[OrderManagementSystem], "orderManagementSystem")
val filter4 = system.actorOf(Props(classOf[Deduplicator], filter5), "deduplicator")
val filter3 = system.actorOf(Props(classOf[Authenticator], filter4), "authenticator")
val filter2 = system.actorOf(Props(classOf[Decrypter], filter3), "decrypter")
val filter1 = system.actorOf(Props(classOf[OrderAcceptanceEndpoint], filter2), "orderAcceptanceEndpoint")
filter1 ! rawOrderBytes
filter1 ! rawOrderBytes
println("PipesAndFilters: is completed.")
system.terminate()
}
消息有效期
package io.binglau.scala.akka.demo
import java.util.Date
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import scala.concurrent.duration.Duration
import scala.util.Random
import scala.concurrent.ExecutionContext.Implicits.global
// 将过期判断操作信息加入消息中
trait ExpiringMessage {
val occurredOn = System.currentTimeMillis()
val timeToLive: Long
def isExpired: Boolean = {
val elapsed = System.currentTimeMillis() - occurredOn
elapsed > timeToLive
}
}
case class PlaceOrder(id: String, itemId: String, price: Double, timeToLive: Long) extends ExpiringMessage
// 模拟各种原因导致的消息传输协议延迟
class PurchaseRouter(purchaseAgent: ActorRef) extends Actor {
val random = new Random((new Date()).getTime)
def receive = {
case message: Any =>
val millis = random.nextInt(100) + 1
println(s"PurchaseRouter: delaying delivery of $message for $millis milliseconds")
val duration = Duration.create(millis, TimeUnit.MILLISECONDS)
context.system.scheduler.scheduleOnce(duration, purchaseAgent, message)
}
}
class PurchaseAgent extends Actor {
def receive = {
case placeOrder: PlaceOrder =>
if (placeOrder.isExpired) {
context.system.deadLetters ! placeOrder
println(s"PurchaseAgent: delivered expired $placeOrder to dead letters")
} else {
println(s"PurchaseAgent: placing order for $placeOrder")
}
case message: Any =>
println(s"PurchaseAgent: received unexpected: $message")
}
}
object MessageExpirationDemo extends App{
val system = ActorSystem("demo")
val purchaseAgent = system.actorOf(Props[PurchaseAgent], "purchaseAgent")
val purchaseRouter = system.actorOf(Props(classOf[PurchaseRouter], purchaseAgent), "purchaseRouter")
purchaseRouter ! PlaceOrder("1", "11", 50.00, 1000)
purchaseRouter ! PlaceOrder("2", "22", 250.00, 100)
purchaseRouter ! PlaceOrder("3", "33", 32.95, 10)
Thread.sleep(3000)
println("MessageExpiration: is completed.")
system.terminate()
}
参考文档
Akka 官方文档
《响应式架构——消息模式 Actor 实现与 Scala、Akka 应用集成》
分析一波 Java 线程池
Executors.executeCase
public class FixedThreadPool {
public static void main(String[] args) {
/**
* Creates a thread pool that reuses a fixed number of threads operating
* off a shared unbounded queue, using the provided ThreadFactory to create
* new threads when needed. At any point, at most nThreads threads will be
* active processing tasks.
*/
ExecutorService exec = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++)
exec.execute(new LiftOff());
exec.shutdown();
}
}
public class LiftOff implements Runnable {
protected int countDown = 10; // Default
private static int taskCount = 0;
private final int id = taskCount++;
public LiftOff() {}
public LiftOff(int countDown) {
this.countDown = countDown;
}
public String status() {
return "#" + id + "(" +
(countDown > 0 ? countDown : "Liftoff!") + "), ";
}
public void run() {
while(countDown-- > 0) {
System.out.print(status());
Thread.yield();
}
}
}
newFixedThreadPool
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
/**
* The default rejected execution handler
*/
private static final RejectedExecutionHandler defaultHandler =
new AbortPolicy(); // 策略
ThreadFactory(Default)
/**
* Returns a default thread factory used to create new threads.
* This factory creates all new threads used by an Executor in the
* same {@link ThreadGroup}. If there is a {@link
* java.lang.SecurityManager}, it uses the group of {@link
* System#getSecurityManager}, else the group of the thread
* invoking this {@code defaultThreadFactory} method. Each new
* thread is created as a non-daemon thread with priority set to
* the smaller of {@code Thread.NORM_PRIORITY} and the maximum
* priority permitted in the thread group. New threads have names
* accessible via {@link Thread#getName} of
* <em>pool-N-thread-M</em>, where <em>N</em> is the sequence
* number of this factory, and <em>M</em> is the sequence number
* of the thread created by this factory.
* @return a thread factory
*/
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
ThreadPoolExecutor 构造器
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory and rejected execution handler.
* It may be more convenient to use one of the {@link Executors} factory
* methods instead of this general purpose constructor.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue} is null
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param threadFactory the factory to use when the executor
* creates a new thread
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code threadFactory} or {@code handler} is null
*/
/**
*
workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQuene;
4、priorityBlockingQuene:具有优先级的无界阻塞队列;
handler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
execute
/**
* Executes the given task sometime in the future. The task
* may execute in a new thread or in an existing pooled thread.
*
* If the task cannot be submitted for execution, either because this
* executor has been shutdown or because its capacity has been reached,
* the task is handled by the current {@code RejectedExecutionHandler}.
*
* @param command the task to execute
* @throws RejectedExecutionException at discretion of
* {@code RejectedExecutionHandler}, if the task
* cannot be accepted for execution
* @throws NullPointerException if {@code command} is null
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
// private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
/**
AtomicInteger变量ctl的功能非常强大:利用低29位表示线程池中线程数,通过高3位表示线程池的运行状态:
1、RUNNING:-1 << COUNT_BITS,即高3位为111,该状态的线程池会接收新任务,并处理阻塞队列中的任务;
2、SHUTDOWN: 0 << COUNT_BITS,即高3位为000,该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
3、STOP : 1 << COUNT_BITS,即高3位为001,该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
4、TIDYING : 2 << COUNT_BITS,即高3位为010;
5、TERMINATED: 3 << COUNT_BITS,即高3位为011;
*/
// workerCountOf方法根据ctl的低29位,得到线程池的当前线程数,如果线程数小于corePoolSize,则执行addWorker方法创建新的线程执行任务
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果线程池处于RUNNING状态,且把提交的任务成功放入阻塞队列中
if (isRunning(c) && workQueue.offer(command)) {
// 再次检查线程池的状态,如果线程池没有RUNNING,且成功从阻塞队列中删除任务,则执行reject方法处理任务
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果得到线程池数量等于0,则添加一条新线程,来替代当前线程,继续去执行队列中的任务.
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 执行addWorker方法创建新的线程执行任务, 如果addWoker执行失败,则执行reject方法处理任务
else if (!addWorker(command, false))
reject(command);
}
addWorker
/*
* Methods for creating, running and cleaning up after workers
*/
/**
* Checks if a new worker can be added with respect to current
* pool state and the given bound (either core or maximum). If so,
* the worker count is adjusted accordingly, and, if possible, a
* new worker is created and started, running firstTask as its
* first task. This method returns false if the pool is stopped or
* eligible to shut down. It also returns false if the thread
* factory fails to create a thread when asked. If the thread
* creation fails, either due to the thread factory returning
* null, or due to an exception (typically OutOfMemoryError in
* Thread.start()), we roll back cleanly.
*
* @param firstTask the task the new thread should run first (or
* null if none). Workers are created with an initial first task
* (in method execute()) to bypass queuing when there are fewer
* than corePoolSize threads (in which case we always start one),
* or when the queue is full (in which case we must bypass queue).
* Initially idle threads are usually created via
* prestartCoreThread or to replace other dying workers.
*
* @param core if true use corePoolSize as bound, else
* maximumPoolSize. (A boolean indicator is used here rather than a
* value to ensure reads of fresh values after checking other pool
* state).
* @return true if successful
*/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
// 判断线程池的状态,如果线程池的状态值大于或等SHUTDOWN,则不处理提交的任务,直接返回
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
// 通过参数core判断当前需要创建的线程是否为核心线程,如果core为true,且当前线程数小于corePoolSize,则跳出循环,开始创建新的线程
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 通过Worker类来实现线程工作流程,具体实现看下一个代码块
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 在ReentrantLock锁的保证下,将 Worker 实例插入 HaseSet(workers)
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 执行 Worker
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
Worker
/**
* Class Worker mainly maintains interrupt control state for
* threads running tasks, along with other minor bookkeeping.
* This class opportunistically extends AbstractQueuedSynchronizer
* to simplify acquiring and releasing a lock surrounding each
* task execution. This protects against interrupts that are
* intended to wake up a worker thread waiting for a task from
* instead interrupting a task being run. We implement a simple
* non-reentrant mutual exclusion lock rather than use
* ReentrantLock because we do not want worker tasks to be able to
* reacquire the lock when they invoke pool control methods like
* setCorePoolSize. Additionally, to suppress interrupts until
* the thread actually starts running tasks, we initialize lock
* state to a negative value, and clear it upon start (in
* runWorker).
*/
// 继承了AQS类,可以方便的实现工作线程的中止操作
// 实现了Runnable接口,可以将自身作为一个任务在工作线程中执行;
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
// 当前提交的任务firstTask作为参数传入Worker的构造方法
// 从Woker类的构造方法实现可以发现:线程工厂在创建线程thread时,将Woker实例本身this作为参数传入,当执行start方法启动线程thread时,本质是执行了Worker的runWorker方法。
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
runWorker(线程执行核心处)
/**
* Main worker run loop. Repeatedly gets tasks from queue and
* executes them, while coping with a number of issues:
*
* 1. We may start out with an initial task, in which case we
* don't need to get the first one. Otherwise, as long as pool is
* running, we get tasks from getTask. If it returns null then the
* worker exits due to changed pool state or configuration
* parameters. Other exits result from exception throws in
* external code, in which case completedAbruptly holds, which
* usually leads processWorkerExit to replace this thread.
*
* 2. Before running any task, the lock is acquired to prevent
* other pool interrupts while the task is executing, and then we
* ensure that unless pool is stopping, this thread does not have
* its interrupt set.
*
* 3. Each task run is preceded by a call to beforeExecute, which
* might throw an exception, in which case we cause thread to die
* (breaking loop with completedAbruptly true) without processing
* the task.
*
* 4. Assuming beforeExecute completes normally, we run the task,
* gathering any of its thrown exceptions to send to afterExecute.
* We separately handle RuntimeException, Error (both of which the
* specs guarantee that we trap) and arbitrary Throwables.
* Because we cannot rethrow Throwables within Runnable.run, we
* wrap them within Errors on the way out (to the thread's
* UncaughtExceptionHandler). Any thrown exception also
* conservatively causes thread to die.
*
* 5. After task.run completes, we call afterExecute, which may
* also throw an exception, which will also cause thread to
* die. According to JLS Sec 14.20, this exception is the one that
* will be in effect even if task.run throws.
*
* The net effect of the exception mechanics is that afterExecute
* and the thread's UncaughtExceptionHandler have as accurate
* information as we can provide about any problems encountered by
* user code.
*
* @param w the worker
*/
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 获取第一个任务firstTask
Runnable task = w.firstTask;
w.firstTask = null;
// 线程启动之后,通过unlock方法释放锁,设置AQS的state为0,表示运行中断
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// firstTask执行完成之后,通过getTask方法从阻塞队列中获取等待的任务,如果队列中没有任务,getTask方法会被阻塞并挂起,不会占用cpu资源;
while (task != null || (task = getTask()) != null) {
// 在执行任务之前,会进行加锁操作,任务执行完会释放锁
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 在执行任务的前后,可以根据业务场景自定义beforeExecute和afterExecute方法;
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行任务的run方法
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 在执行任务的前后,可以根据业务场景自定义beforeExecute和afterExecute方法;
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
获取队列任务 getTaks()
/**
* Performs blocking or timed wait for a task, depending on
* current configuration settings, or returns null if this worker
* must exit because of any of:
* 1. There are more than maximumPoolSize workers (due to
* a call to setMaximumPoolSize).
* 2. The pool is stopped.
* 3. The pool is shutdown and the queue is empty.
* 4. This worker timed out waiting for a task, and timed-out
* workers are subject to termination (that is,
* {@code allowCoreThreadTimeOut || workerCount > corePoolSize})
* both before and after the timed wait, and if the queue is
* non-empty, this worker is not the last thread in the pool.
*
* @return task, or null if the worker must exit, in which case
* workerCount is decremented
*/
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
// 整个getTask操作在自旋下完成
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
try {
Runnable r = timed ?
// workQueue.poll:如果在keepAliveTime时间内,阻塞队列还是没有任务,则返回null;
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
// workQueue.take:如果阻塞队列为空,当前线程会被挂起等待;当队列中有任务加入时,线程被唤醒,take方法返回任务,并执行;
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
Future和Callable实现
case
private static void main(String[] args) {
ExecutorService execute = Executors.newFixedThreadPool(10);
Future<String> future = execute.submit(new Task());
System.out.println("do other things");
try {
String result = future.get();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
static class Task implements Callable<String> {
@Override
public String call() throws Exception {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "this is future case";
}
}
submit
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
// 通过submit方法提交的Callable任务会被封装成了一个FutureTask对象。
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
FutureTask
- FutureTask在不同阶段拥有不同的状态state,初始化为NEW;
- FutureTask类实现了Runnable接口,这样就可以通过Executor.execute方法提交FutureTask到线程池中等待被执行,最终执行的是FutureTask的run方法;
public class FutureTask<V> implements RunnableFuture<V> {
/*
* Revision notes: This differs from previous versions of this
* class that relied on AbstractQueuedSynchronizer, mainly to
* avoid surprising users about retaining interrupt status during
* cancellation races. Sync control in the current design relies
* on a "state" field updated via CAS to track completion, along
* with a simple Treiber stack to hold waiting threads.
*
* Style note: As usual, we bypass overhead of using
* AtomicXFieldUpdaters and instead directly use Unsafe intrinsics.
*/
/**
* The run state of this task, initially NEW. The run state
* transitions to a terminal state only in methods set,
* setException, and cancel. During completion, state may take on
* transient values of COMPLETING (while outcome is being set) or
* INTERRUPTING (only while interrupting the runner to satisfy a
* cancel(true)). Transitions from these intermediate to final
* states use cheaper ordered/lazy writes because values are unique
* and cannot be further modified.
*
* Possible state transitions:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
/** The underlying callable; nulled out after running */
private Callable<V> callable;
/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes
/** The thread running the callable; CASed during run() */
private volatile Thread runner;
/** Treiber stack of waiting threads */
private volatile WaitNode waiters;
/**
* Creates a {@code FutureTask} that will, upon running, execute the
* given {@code Callable}.
*
* @param callable the callable task
* @throws NullPointerException if the callable is null
*/
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
FutureTask.get()
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
/**
* Returns result or throws exception for completed task.
*
* @param s completed state value
*/
@SuppressWarnings("unchecked")
private V report(int s) throws ExecutionException {
// 记住这个 outcome
Object x = outcome;
if (s == NORMAL)
return (V)x;
if (s >= CANCELLED)
throw new CancellationException();
throw new ExecutionException((Throwable)x);
}
内部通过awaitDone方法对主线程进行阻塞,然后由 report()
返回具体实现如下:
/**
* Awaits completion or aborts on interrupt or timeout.
*
* @param timed true if use timed waits
* @param nanos time to wait, if timed
* @return state upon completion
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
// 如果主线程被中断,则抛出中断异常
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}
// 判断FutureTask当前的state,如果大于COMPLETING,说明任务已经执行完成,则直接返回
int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
// 如果当前state等于COMPLETING,说明任务已经执行完,这时主线程只需通过yield方法让出cpu资源,等待state变成NORMAL
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
// 通过WaitNode类封装当前线程,并通过UNSAFE添加到waiters链表
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
// 最终通过LockSupport的park或parkNanos挂起线程
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}
FutureTask.run(),上文分析的 execute 最终执行的方法
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
// 通过执行Callable任务的call方法
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 如果call执行有异常,则通过setException保存异常
setException(ex);
}
// 如果call执行成功,则通过set方法保存结果
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
FutureTask.set()
/**
* Sets the result of this future to the given value unless
* this future has already been set or has been cancelled.
*
* <p>This method is invoked internally by the {@link #run} method
* upon successful completion of the computation.
*
* @param v the value
*/
protected void set(V v) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 还记得那个 outcome 吗
outcome = v;
// 通过UNSAFE修改FutureTask的状态,并执行finishCompletion方法通知主线程任务已经执行完成
UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
finishCompletion();
}
}
FutureTask.setException()
/**
* Causes this future to report an {@link ExecutionException}
* with the given throwable as its cause, unless this future has
* already been set or has been cancelled.
*
* <p>This method is invoked internally by the {@link #run} method
* upon failure of the computation.
*
* @param t the cause of failure
*/
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
FutureTask.finishCompletion()
/**
* Removes and signals all waiting threads, invokes done(), and
* nulls out callable.
*/
private void finishCompletion() {
// assert state > COMPLETING;
for (WaitNode q; (q = waiters) != null;) {
if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
for (;;) {
Thread t = q.thread;
if (t != null) {
q.thread = null;
// FutureTask任务执行完成后,通过UNSAFE设置waiters的值,并通过LockSupport类unpark方法唤醒主线程
LockSupport.unpark(t);
}
// 执行FutureTask类的get方法时,会把主线程封装成WaitNode节点并保存在waiters链表中
WaitNode next = q.next;
if (next == null)
break;
q.next = null; // unlink to help gc
q = next;
}
break;
}
}
done();
callable = null; // to reduce footprint
}
垃圾回收与内存分配策略
谈及 GC 自然是需要先谈及如何触发 GC,GC 作为垃圾回收器,自然是需要定义内存中『垃圾』的,那么怎么定义自然就成了关键。总体来说有以下的思路。
标记算法
计数引用
简单思路,引用 +1,失效 -1。对于循环引用无效。比较简单且 JVM 不是使用这种方法的,所以不进行深入介绍了。
可达性分析
基本思路:它将整个内存中的对象看做一个树的形状,只要抓住根节点(GC Roots
)并从根节点向下会形成无数子树(引用链(Reference Chain)),当一个对象没有与根节点相关联的子树的时候(这个对象从 GC Roots
不可达)则说明此对象不可用。
在 Java 中,可作为 GC Roots
的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中 JNI (即一般说的 Native 对象)引用的对象。
值得一说的是,Java 中对于引用也是有分级的,当内存空间不足够的时候,我们会从最弱的引用开始释放空间。
- 强引用,类似于
Object obj = new Object()
这类在代码里面常见的。永远不会回收。 - 软引用,有用但并非必需的对象。在系统将要发生内存溢出异常前,会将这些对象列入回收范围之中进行二次回收。提供了
SoftReference
类来实现软引用。 - 弱引用,只能生存到下一次垃圾收集发生之前。
WeakReference
类实现。 - 虚引用,将一个对象设置为虚引用唯一目的就是能在这个对象呗收集器回收时受到一个系统通知。
PhantomReference
类实现。
要知道一点,就算被判定为不可达对象了,也并非非死不可,这时候处于准备去死的状态吧。
要真正宣告一个对象死亡,至少要经历两次标记过程:
- 是否有必要执行(只能执行一次)
finalize()
方法,如果有则会放入一个F-Queue
队列,由一个低优先级的Finalize
线程来执行它,但不会保证执行完成(防止阻塞了F-Queue
队列导致内存回收崩溃)。 - GC 将在
F-Queue
中的对象进行第二次标记,如果对象想拯救自己,则需要在finalize
方法给自己与GC Roots
再建立上关联。
垃圾收集算法
标记清除
标记:之前介绍的可达性算法标记。完成之后进行清除。
缺点:
- 标记和清除两个过程的效率都不高
- 产生空间碎片
复制算法
将可用内存按容量划分为大小相等的两块(不一定会大小相等),每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点:内存占用太多。
新生代的垃圾收集
将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
标记整理算法
标记过程仍然与『标记-清除』算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
缺点:效率不高。
垃圾收集器
安全点
在 HotSpot 中有一组叫做 OopMap
的数据结构用于当 『Stop World』时候获取哪些地方存放对象引用及对象类型(用于释放内存大小)。在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
为了减少使用 OopMap
指令的生成(占用大量空间),只需要在『特殊位置』记录了这些信息,这些位置就被称为安全点。
即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停。Safepoint的选定既不能太少以致于让GC等待时间太长,也不能过于频繁以致于过分增大运行时的负荷。所以,安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。
HotSpot 垃圾收集器
在这里只会详细介绍 G1,如果有其他垃圾收集器需求可以自行查询解决(网上充斥了大量的资料)。
Serial
重点提要:
- 新生代
- 单线程
- 简单高效,适合 Client
ParNew
重点提要:
- 新生代
- Serial 多线程版本
- CMS(老年代) 无法配合 Paraller Scavenge(新生代),所以只能选择 Serial / ParNew (为什么)
Paraller Scavenge
重点提要:
- 新生代
- 可控吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))
- 自适应调节策略(虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量)
Serial Old
重点提要:
- serial 老年代版本
- 使用“标记-整理”算法
Paraller Old
重点提要:
- Paraller Scavenge 老年代版本
- 这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”
- 在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器
CMS
重点提要:
- 老年代
- 并发收集、低停顿
- 对CPU资源非常敏感
- 无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次FullGC的产生
- CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。
G1(Garbage-First Garbage Collector) 垃圾收集器
之所以重点说 G1 是因为在 Java 9 中 G1 已经被作为默认的垃圾收集器了,且其理念与之前的收集器有很大的区别。
G1 的特点
G1 是一个“服务器风格(server-style)”的垃圾回收器,它主要有下面的这些属性:
- 并行和并发。 G1 可以从今天最新的硬件中获得并行的能力。它能够使用所有可用的CPU(CPU多核,硬件多线程等)来加速它的 “stop-the-world” 机制(这个机制简称STW,即,在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集帮助器线程之外的线程都被挂起)。
- 分代处理。 就像其它的HotSpot 垃圾回收器,G1 是分代的,也就是说,它在处理新分配的对象(年轻代)和已经生存了一段时间的对象(年老代)时会不同,它会更多地考虑一些新创建的对象实例,因为越新创建的就越有最大的可能性被回收,老对象只是偶尔访问一下。对于大多数的Java应用来说,这个机制可以极大地提高回收效率。
- 紧凑内存(碎片整理)。 不像CMS,G1 会对堆进行内存整理。压缩可以消除潜在的内存碎片的问题,这样程序就可以更长时间的平滑运行。
- 预见性的。 G1 比起 CMS 来有更多的预见性。G1 能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。
G1 基本思路
G1 将整个 Java 堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分 Region(不需要连续)的集合。
G1 之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。**G1 跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据运行的收集时间,优先回收价值最大的 Region(也就是 Garbage-First 名称由来)。**这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间内可以获取尽可能高的收集效率。
对象引用不确定在哪个 Region
Region 不是孤立的。一个对象分配在某个 Region 中,它并非只能被本 Region 中的其他对象引用,而是可以与整个 Java 堆任意的对象发生引用关系。那么做可达性分析的时候岂不是还得扫描整个 Java 堆才能保证准确性?这个问题在其他收集器中也存在,但是 G1 中尤其突出。
在 G1 收集器中,Region 之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set
来避免全堆扫描的。G1 中每个 Region 都有一个与之对应的 Remembered Set
,虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set
之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set
即可保证不对全堆扫描也不会有遗漏。
G1 具体流程(不考虑维护 Remembered Set)
- 初始标记:仅仅标记 GC Roots 能关联到的对象,并且修改 TAMS(Next Top at Mark Start) 的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建对象。此处需要 STW。
- 并发标记:从 GC Roots 开始对堆中的对象进行可达性分析,找出存活对象。并行执行。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那部分标记记录。虚拟机会将这段时间的改变记录在线程
Remembered Set Logs
中,需要将它的数据合并到Remembered Set
中。需要 STW。 - 筛选回收:首先对各个 Region 的回收价值和成本进行排序,然后根据用户所期望的 GC 停顿时间来制定回收计划。并发执行。
G1 diff CMS
- G1在压缩空间方面有优势
- G1通过将内存空间分成区域(Region)的方式避免内存碎片问题
- Eden, Survivor, Old区不再固定、在内存使用效率上来说更灵活
- G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象
- G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做
- G1会在Young GC中使用、而CMS只能在OLD区使用
G1 适用场景
- 服务端多核CPU、JVM内存占用较大的应用(至少大于6G)
- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期;防止高并发下应用雪崩现象
垃圾收集器参数总结
注明:此处由于是书上(JDK7版本)描述,所以有些 JVM 参数可能过时了,大家可以参照 JDK 9 常用参数(TODO)来使用
内存分配与回收策略
对象的内存分配,往大方向讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
对象优先在 Eden 分配
重点提要:
- 当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
- 虚拟机提供了-XX:+PrintGCDetails这个收集器日志参数,告诉虚拟机在发生垃圾收集行为时打印内存回收日志,并且在进程退出的时候输出当前的内存各区域分配情况。
- Minor GC 是指新生代发生的 GC,非常频繁;Full GC/Major GC 是指老年代发生的 GC,速度很慢。
大对象直接进入老年代
重点提要:
- 所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象)。
- 避免『短命』大对象
- 虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象将进入老年代
重点提要:
- 虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。
- 对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
动态对象年龄判定
重点提要:
- 为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
分配空间担保
重点提要:
- 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
GC 日志阅读实例(G1)
Young GC
{Heap before GC invocations=12 (full 1):
# 这行表示使用了G1垃圾收集器,total heap 3145728K,使用了336645K。
garbage-first heap total 3145728K, used 336645K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
# Region大小为1M,青年代占用了172个(共176128K),幸存区占用了13个(共13312K)。
region size 1024K, 172 young (176128K), 13 survivors (13312K)
# java 8的新特性,去掉永久区,添加了元数据区,这块不是本文重点,不再赘述。需要注意的是,之所以有committed和reserved,是因为没有设置MetaspaceSize=MaxMetaspaceSize。
Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
# GC原因,新生代minor GC。
2014-11-14T17:57:23.654+0800: 27.884: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 11534336 bytes, new threshold 15 (max 15)
- age 1: 5011600 bytes, 5011600 total
# 发生minor GC和full GC时,所有相关region都是要回收的。而发生并发GC时,会根据目标停顿时间动态选择部分垃圾对并多的Region回收,这一步就是选择Region。_pending_cards是关于RSet的Card Table。predicted base time是预测的扫描card table时间。
27.884: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 1461, predicted base time: 35.25 ms, remaining time: 64.75 ms, target pause time: 100.00 ms]
# 这一步是添加Region到collection set,新生代一共159个Region,13个幸存区Region,这也和之前的(172 young (176128K), 13 survivors (13312K))吻合。预计收集时间是44.09 ms。
27.884: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 159 regions, survivors: 13 regions, predicted young region time: 44.09 ms]
# 这一步是对上面两步的总结。预计总收集时间79.34ms。
27.884: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 159 regions, survivors: 13 regions, old: 0 regions, predicted pause time: 79.34 ms, target pause time: 100.00 ms]
, 0.0158389 secs]
# 由于收集过程是多线程并行(并发)进行,这里是4个线程,总共耗时8.1ms(wall clock time)
[Parallel Time: 8.1 ms, GC Workers: 4]
# 收集线程开始的时间,使用的是相对时间,Min是最早开始时间,Avg是平均开始时间,Max是最晚开始时间,Diff是Max-Min
[GC Worker Start (ms): Min: 27884.5, Avg: 27884.5, Max: 27884.5, Diff: 0.1]
# 扫描Roots花费的时间,Sum表示total cpu time,下同。
[Ext Root Scanning (ms): Min: 0.4, Avg: 0.8, Max: 1.2, Diff: 0.8, Sum: 3.1]
# Update RS (ms)是每个线程花费在更新Remembered Set上的时间。
[Update RS (ms): Min: 0.0, Avg: 0.3, Max: 0.6, Diff: 0.6, Sum: 1.4]
[Processed Buffers: Min: 0, Avg: 2.8, Max: 5, Diff: 5, Sum: 11]
# 扫描CS中的region对应的RSet,因为RSet是points-into,所以这样实现避免了扫描old generadion region,但是会产生float garbage。
[Scan RS (ms): Min: 0.0, Avg: 0.1, Max: 0.1, Diff: 0.1, Sum: 0.3]
# 扫描code root耗时。code root指的是经过JIT编译后的代码里,引用了heap中的对象。引用关系保存在RSet中。
[Code Root Scanning (ms): Min: 0.0, Avg: 0.1, Max: 0.2, Diff: 0.2, Sum: 0.6]
# 拷贝活的对象到新region的耗时。
[Object Copy (ms): Min: 4.9, Avg: 5.1, Max: 5.2, Diff: 0.3, Sum: 20.4]
# 线程结束,在结束前,它会检查其他线程是否还有未扫描完的引用,如果有,则"偷"过来,完成后再申请结束,这个时间是线程之前互相同步所花费的时间。
[Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
# 花费在其他工作上(未列出)的时间。
[GC Worker Other (ms): Min: 0.0, Avg: 0.4, Max: 1.3, Diff: 1.3, Sum: 1.4]
# 每个线程花费的时间和。
[GC Worker Total (ms): Min: 6.4, Avg: 6.8, Max: 7.8, Diff: 1.4, Sum: 27.2]
# 每个线程结束的时间。
[GC Worker End (ms): Min: 27891.0, Avg: 27891.3, Max: 27892.3, Diff: 1.3]
# 用来将code root修正到正确的evacuate之后的对象位置所花费的时间。
[Code Root Fixup: 0.5 ms]
# 更新code root 引用的耗时,code root中的引用因为对象的evacuation而需要更新。
[Code Root Migration: 1.3 ms]
# 清除code root的耗时,code root中的引用已经失效,不再指向Region中的对象,所以需要被清除。
[Code Root Purge: 0.0 ms]
# 清除card table的耗时。
[Clear CT: 0.2 ms]
# 其他事项共耗时5.8ms,其他事项包括选择CSet,处理已用对象,引用入ReferenceQueues,释放CSet中的region到free list。
[Other: 5.8 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 5.0 ms]
[Ref Enq: 0.1 ms]
[Redirty Cards: 0.0 ms]
[Free CSet: 0.2 ms]
# 新生代清空了,下次扩容到301MB。
[Eden: 159.0M(159.0M)->0.0B(301.0M) Survivors: 13.0M->11.0M Heap: 328.8M(3072.0M)->167.3M(3072.0M)]
Heap after GC invocations=13 (full 1):
garbage-first heap total 3145728K, used 171269K [0x0000000700000000, 0x00000007c0000000, 0x00000007c0000000)
region size 1024K, 11 young (11264K), 11 survivors (11264K)
Metaspace used 29944K, capacity 30196K, committed 30464K, reserved 1077248K
class space used 3391K, capacity 3480K, committed 3584K, reserved 1048576K
}
[Times: user=0.05 sys=0.01, real=0.02 secs]
参考文章
《深入理解Java虚拟机》
Python整型对象创建
整数类型
Python3 中无论整数还是长整数统统使用 PyLongObject 类型来代替
typedef struct _longobject PyLongObject; /* Revealed in longintrepr.h */
/* Long integer representation.
The absolute value of a number is equal to
SUM(for i=0 through abs(ob_size)-1) ob_digit[i] * 2**(SHIFT*i)
Negative numbers are represented with ob_size < 0;
zero is represented by ob_size == 0.
In a normalized number, ob_digit[abs(ob_size)-1] (the most significant
digit) is never zero. Also, in all cases, for all valid i,
0 <= ob_digit[i] <= MASK.
The allocation function takes care of allocating extra memory
so that ob_digit[0] ... ob_digit[abs(ob_size)-1] are actually available.
CAUTION: Generic code manipulating subtypes of PyVarObject has to
aware that ints abuse ob_size's sign bit.
*/
struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
#define PyObject_VAR_HEAD PyVarObject ob_base;
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
/* Nothing is actually declared to be a PyObject, but every pointer to
* a Python object can be cast to a PyObject*. This is inheritance built
* by hand. Similarly every pointer to a variable-size Python object can,
* in addition, be cast to PyVarObject*.
*/
typedef struct _object {
_PyObject_HEAD_EXTRA
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
#ifdef Py_TRACE_REFS
/* Define pointers to support a doubly-linked list of all live heap objects. */
#define _PyObject_HEAD_EXTRA \
struct _object *_ob_next; \
struct _object *_ob_prev;
#define _PyObject_EXTRA_INIT 0, 0,
#else
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
#endif
由于Py_TRACE_REFS
在release状态下是不会定义的,所以整个数据结构就会特别简单:
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
小数优化
#ifndef NSMALLPOSINTS
#define NSMALLPOSINTS 257
#endif
#ifndef NSMALLNEGINTS
#define NSMALLNEGINTS 5
#endif
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
/* Small integers are preallocated in this array so that they
can be shared.
The integers that are preallocated are those in the range
-NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
*/
static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
#ifdef COUNT_ALLOCS
Py_ssize_t quick_int_allocs, quick_neg_int_allocs;
#endif
static PyObject *
get_small_int(sdigit ival)
{
PyObject *v;
assert(-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS);
v = (PyObject *)&small_ints[ival + NSMALLNEGINTS];
Py_INCREF(v);
#ifdef COUNT_ALLOCS
if (ival >= 0)
quick_int_allocs++;
else
quick_neg_int_allocs++;
#endif
return v;
}
#define CHECK_SMALL_INT(ival) \
do if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) { \
return get_small_int((sdigit)ival); \
} while(0)
创建函数
PyLongObject *
_PyLong_New(Py_ssize_t size)
{
PyLongObject *result;
/* Number of bytes needed is: offsetof(PyLongObject, ob_digit) +
sizeof(digit)*size. Previous incarnations of this code used
sizeof(PyVarObject) instead of the offsetof, but this risks being
incorrect in the presence of padding between the PyVarObject header
and the digits. */
if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
PyErr_SetString(PyExc_OverflowError,
"too many digits in integer");
return NULL;
}
// offsetof(type, member-designator) 会生成一个类型为 size_t 的整型常量,它是一个结构成员相对于结构开头的字节偏移量。成员是由 member-designator 给定的,结构的名称是在 type 中给定的。
result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
size*sizeof(digit));
if (!result) {
PyErr_NoMemory();
return NULL;
}
return (PyLongObject*)PyObject_INIT_VAR(result, &PyLong_Type, size);
}
其中 PyObject_MALLOC
是与 WITH_PYMALLOC
定义相关
Pymalloc, a specialized object allocator written by Vladimir Marangozov, was a feature added to Python 2.1. Pymalloc is intended to be faster than the system malloc() and to have less memory overhead for allocation patterns typical of Python programs. The allocator uses C's malloc() function to get large pools of memory and then fulfills smaller memory requests from these pools.
In 2.1 and 2.2, pymalloc was an experimental feature and wasn't enabled by default; you had to explicitly enable it when compiling Python by providing the --with-pymalloc option to the configure script. In 2.3, pymalloc has had further enhancements and is now enabled by default; you'll have to supply**--without-pymalloc** to disable it.
可以看出这东西是默认开启的,所以从代码可知调用的是_PyObject_Malloc
方法,再追踪源码发现其最终调用的是如下方法:
void *
PyObject_Malloc(size_t size)
{
/* see PyMem_RawMalloc() */
if (size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyObject.malloc(_PyObject.ctx, size);
}
static void *
_PyObject_Malloc(void *ctx, size_t nbytes)
{
return _PyObject_Alloc(0, ctx, 1, nbytes);
}
/* malloc. Note that nbytes==0 tries to return a non-NULL pointer, distinct
* from all other currently live pointers. This may not be possible.
*/
/*
* The basic blocks are ordered by decreasing execution frequency,
* which minimizes the number of jumps in the most common cases,
* improves branching prediction and instruction scheduling (small
* block allocations typically result in a couple of instructions).
* Unless the optimizer reorders everything, being too smart...
*/
static void *
_PyObject_Alloc(int use_calloc, void *ctx, size_t nelem, size_t elsize)
{
/*
解释一下 ctx, 这是 PyMemAllocatorEx 的 ctx,
static PyMemAllocatorEx _PyObject = {
#ifdef Py_DEBUG
&_PyMem_Debug.obj, PYDBG_FUNCS
#else
NULL, PYOBJ_FUNCS
#endif
};
来定义,可以看出,这个是为了 Debug 来操作的
*/
size_t nbytes;
block *bp;
poolp pool;
poolp next;
uint size;
// 记录分配了一块 block
_Py_AllocatedBlocks++;
assert(nelem <= PY_SSIZE_T_MAX / elsize);
// 创建对象个数乘以创建每个对象所需要的字节数
nbytes = nelem * elsize;
// 是否开启 valgrind 来调试程序
#ifdef WITH_VALGRIND
if (UNLIKELY(running_on_valgrind == -1))
running_on_valgrind = RUNNING_ON_VALGRIND;
if (UNLIKELY(running_on_valgrind))
goto redirect;
#endif
if (nelem == 0 || elsize == 0)
goto redirect;
// 小块内存与大块内存分配分界处
if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
// 这里有些不能理解,看注释是
/*
* Locking
*
* To reduce lock contention, it would probably be better to refine the
* crude function locking with per size class locking. I'm not positive
* however, whether it's worth switching to such locking policy because
* of the performance penalty it might introduce.
*
* The following macros describe the simplest (should also be the fastest)
* lock object on a particular platform and the init/fini/lock/unlock
* operations on it. The locks defined here are not expected to be recursive
* because it is assumed that they will always be called in the order:
* INIT, [LOCK, UNLOCK]*, FINI.
*/
/*
* Python's threads are serialized, so object malloc locking is disabled.
*/
// 但是不能理解其加锁方式
LOCK();
/*
* Most frequent paths first
*/
// 得到 size_class_index
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
// 从 userdpool 中找到匹配的pool然后准备存到里面去
pool = usedpools[size + size];
if (pool != pool->nextpool) {
// 这个是最终都要进入的地方
// 如果 usedpool 中有可用的 pool
/*
* There is a used pool for this size class.
* Pick up the head block of its free list.
*/
++pool->ref.count;
bp = pool->freeblock;
assert(bp != NULL);
if ((pool->freeblock = *(block **)bp) != NULL) {
UNLOCK();
if (use_calloc)
memset(bp, 0, nbytes);
return (void *)bp;
}
/*
* Reached the end of the free list, try to extend it.
*/
if (pool->nextoffset <= pool->maxnextoffset) {
/* There is room for another block. */
pool->freeblock = (block*)pool +
pool->nextoffset;
pool->nextoffset += INDEX2SIZE(size);
*(block **)(pool->freeblock) = NULL;
UNLOCK();
if (use_calloc)
memset(bp, 0, nbytes);
return (void *)bp;
}
/* Pool is full, unlink from used pools. */
next = pool->nextpool;
pool = pool->prevpool;
next->prevpool = pool;
pool->nextpool = next;
UNLOCK();
if (use_calloc)
memset(bp, 0, nbytes);
// bp 返回的实际是一个地址,这个地址之后有将近 4KB 的内存实际上都是可用的,
// 但是可用肯定申请内存的函数只会使用[bp, bp+size] 这个区间的内存,
// 这是由 size class index 可用保证的。
return (void *)bp;
}
//usedpools中无可用pool,尝试获取empty状态pool
/* There isn't a pool of the right size class immediately
* available: use a free pool.
*/
if (usable_arenas == NULL) {
// 如果usable_arenas链表为空,则创建链表
/* No arena has a free pool: allocate a new arena. */
// WITH_MEMORY_LIMITS 编译时候打开会激活 SMALL_MEMORY_LIMIT 符号,该符号限制了 arena 的个数
#ifdef WITH_MEMORY_LIMITS
if (narenas_currently_allocated >= MAX_ARENAS) {
UNLOCK();
goto redirect;
}
#endif
// 申请新的arena_object,并放入usable_arenas链表
usable_arenas = new_arena();
if (usable_arenas == NULL) {
UNLOCK();
goto redirect;
}
usable_arenas->nextarena =
usable_arenas->prevarena = NULL;
}
assert(usable_arenas->address != 0);
/* Try to get a cached free pool. */
// 从usable_arenas链表中第一个arena的freepools中抽取一个可用的pool
pool = usable_arenas->freepools;
if (pool != NULL) {
/* Unlink from cached pools. */
usable_arenas->freepools = pool->nextpool;
/* This arena already had the smallest nfreepools
* value, so decreasing nfreepools doesn't change
* that, and we don't need to rearrange the
* usable_arenas list. However, if the arena has
* become wholly allocated, we need to remove its
* arena_object from usable_arenas.
*/
//调整usable_arenas链表中第一个arena中的可用pool数量
//如果调整后数量为0,则将该arena从usable_arenas链表中摘除
--usable_arenas->nfreepools;
if (usable_arenas->nfreepools == 0) {
/* Wholly allocated: remove. */
assert(usable_arenas->freepools == NULL);
assert(usable_arenas->nextarena == NULL ||
usable_arenas->nextarena->prevarena ==
usable_arenas);
usable_arenas = usable_arenas->nextarena;
if (usable_arenas != NULL) {
usable_arenas->prevarena = NULL;
assert(usable_arenas->address != 0);
}
}
else {
/* nfreepools > 0: it must be that freepools
* isn't NULL, or that we haven't yet carved
* off all the arena's pools for the first
* time.
*/
assert(usable_arenas->freepools != NULL ||
usable_arenas->pool_address <=
(block*)usable_arenas->address +
ARENA_SIZE - POOL_SIZE);
}
init_pool:
/* Frontlink to used pools. */
// 将 pool 放入 usedpool 中
next = usedpools[size + size]; /* == prev */
pool->nextpool = next;
pool->prevpool = next;
next->nextpool = pool;
next->prevpool = pool;
pool->ref.count = 1;
if (pool->szidx == size) {
/* Luckily, this pool last contained blocks
* of the same size class, so its header
* and free list are already initialized.
*/
bp = pool->freeblock;
assert(bp != NULL);
pool->freeblock = *(block **)bp;
UNLOCK();
if (use_calloc)
memset(bp, 0, nbytes);
return (void *)bp;
}
/*
* Initialize the pool header, set up the free list to
* contain just the second block, and return the first
* block.
*/
// 初始化pool header,将freeblock指向第二个block,返回第一个block
pool->szidx = size;
size = INDEX2SIZE(size);
bp = (block *)pool + POOL_OVERHEAD;
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
UNLOCK();
if (use_calloc)
memset(bp, 0, nbytes);
return (void *)bp;
}
/* Carve off a new pool. */
assert(usable_arenas->nfreepools > 0);
assert(usable_arenas->freepools == NULL);
// 从arena中取出一个新的pool
pool = (poolp)usable_arenas->pool_address;
assert((block*)pool <= (block*)usable_arenas->address +
ARENA_SIZE - POOL_SIZE);
// 设置 pool 中的 arenaindex,这个 index 实际上就是 pool 所在的 arena
// 位于 arenas 所指的数组的序号。用于判断一个 block 是否在某个 pool 中。
pool->arenaindex = (uint)(usable_arenas - arenas);
assert(&arenas[pool->arenaindex] == usable_arenas);
// 随后 Python 将新得到的 pool 的 szidx 设置为 0xffff,表示从没管理过 block 集合。
pool->szidx = DUMMY_SIZE_IDX;
// 调整刚获得的 arena 中的 pools 集合,甚至可能调整 usable_arenas
usable_arenas->pool_address += POOL_SIZE;
--usable_arenas->nfreepools;
if (usable_arenas->nfreepools == 0) {
assert(usable_arenas->nextarena == NULL ||
usable_arenas->nextarena->prevarena ==
usable_arenas);
/* Unlink the arena: it is completely allocated. */
usable_arenas = usable_arenas->nextarena;
if (usable_arenas != NULL) {
usable_arenas->prevarena = NULL;
assert(usable_arenas->address != 0);
}
}
goto init_pool;
}
/* The small block allocator ends here. */
redirect:
/* Redirect the original request to the underlying (libc) allocator.
* We jump here on bigger requests, on error in the code above (as a
* last chance to serve the request) or when the max memory limit
* has been reached.
*/
{
void *result;
if (use_calloc)
result = PyMem_RawCalloc(nelem, elsize);
else
result = PyMem_RawMalloc(nbytes);
if (!result)
_Py_AllocatedBlocks--;
return result;
}
}
/* Allocate a new arena. If we run out of memory, return NULL. Else
* allocate a new arena, and return the address of an arena_object
* describing the new arena. It's expected that the caller will set
* `usable_arenas` to the return value.
*/
static struct arena_object*
new_arena(void)
{
struct arena_object* arenaobj;
uint excess; /* number of bytes above pool alignment */
void *address;
static int debug_stats = -1;
if (debug_stats == -1) {
char *opt = Py_GETENV("PYTHONMALLOCSTATS");
debug_stats = (opt != NULL && *opt != '\0');
}
if (debug_stats)
_PyObject_DebugMallocStats(stderr);
// 判断是否需要扩充“未使用的”arena_object列表
if (unused_arena_objects == NULL) {
uint i;
uint numarenas;
size_t nbytes;
/* Double the number of arena objects on each allocation.
* Note that it's possible for `numarenas` to overflow.
*/
// 确定本次需要申请的arena_object的个数,并申请内存
numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
if (numarenas <= maxarenas)
return NULL; /* overflow */
#if SIZEOF_SIZE_T <= SIZEOF_INT
if (numarenas > SIZE_MAX / sizeof(*arenas))
return NULL; /* overflow */
#endif
nbytes = numarenas * sizeof(*arenas);
// realloc() 对 ptr 指向的内存重新分配 size 大小的空间,size 可比原来的大或者小
arenaobj = (struct arena_object *)PyMem_RawRealloc(arenas, nbytes);
if (arenaobj == NULL)
return NULL;
arenas = arenaobj;
/* We might need to fix pointers that were copied. However,
* new_arena only gets called when all the pages in the
* previous arenas are full. Thus, there are *no* pointers
* into the old array. Thus, we don't have to worry about
* invalid pointers. Just to be sure, some asserts:
*/
assert(usable_arenas == NULL);
assert(unused_arena_objects == NULL);
/* Put the new arenas on the unused_arena_objects list. */
for (i = maxarenas; i < numarenas; ++i) {
arenas[i].address = 0; /* mark as unassociated */
arenas[i].nextarena = i < numarenas - 1 ?
&arenas[i+1] : NULL;
}
/* Update globals. */
unused_arena_objects = &arenas[maxarenas];
maxarenas = numarenas;
}
/* Take the next available arena object off the head of the list. */
// 从unused_arena_objects链表中取出一个“未使用的”arena_object
assert(unused_arena_objects != NULL);
arenaobj = unused_arena_objects;
unused_arena_objects = arenaobj->nextarena;
assert(arenaobj->address == 0);
// 申请arena_object管理的内存
// 这里的 alloc 对应 linux 下就是 malloc
address = _PyObject_Arena.alloc(_PyObject_Arena.ctx, ARENA_SIZE);
if (address == NULL) {
/* The allocation failed: return NULL after putting the
* arenaobj back.
*/
arenaobj->nextarena = unused_arena_objects;
unused_arena_objects = arenaobj;
return NULL;
}
arenaobj->address = (uintptr_t)address;
++narenas_currently_allocated;
++ntimes_arena_allocated;
if (narenas_currently_allocated > narenas_highwater)
narenas_highwater = narenas_currently_allocated;
arenaobj->freepools = NULL;
/* pool_address <- first pool-aligned address in the arena
nfreepools <- number of whole pools that fit after alignment */
arenaobj->pool_address = (block*)arenaobj->address;
arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
assert(POOL_SIZE * arenaobj->nfreepools == ARENA_SIZE);
excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
if (excess != 0) {
--arenaobj->nfreepools;
arenaobj->pool_address += POOL_SIZE - excess;
}
arenaobj->ntotalpools = arenaobj->nfreepools;
return arenaobj;
}
具体可以参考以上注释,以上。
参考资料
https://docs.python.org/2.3/whatsnew/section-pymalloc.html
《Python 源码解析》
定时任务实践(Python向)
之所以会有这篇博客其主要原因是因为最近需要写一些脚本用于定时运行,而我个人对于Shell的crontab又是处于看了就忘,忘了就不想看的阶段,Python在我目前看来是最适合替换掉Shell脚本的手段,其中自然也有大量定时运行的技术手段,这篇对于三种可使用不同方法执行定时任务的库做个介绍,仅仅只是简单介绍,如果需要详细了解恐怕还是看其官方文档来得更加适合。
Plan
Plan其实是借用Python创建一条crontab的命令,然后依据crontab来运行
安装
pip install plan
基本用法
- 创建一个Plan实例(
cron = Plan()
) - 向其实例添加一个实时运行的命令,脚本或模块
cron.command('command', every='1.day')
cron.script('script.py', path='/web/yourproject/scripts', every='1.month')
cron.module('calendar', every='feburary', at='day.3')
- 运行(
cron.run()
)
关键方法
无论是command,script或是module其实在Plan中都可以称之为Job,我们甚至可以创建自己的Job,先让我们来了解一下Plan中的Job。
Job
其实Job就相当于crontab中的一条命令,它带有task
, every
, at
, path
, environment
与 output
,这些参数含义大概从名字来看也可猜出一二。
task指定运行任务名
其中every参数是指定其运行周期性,其:
[1-60].minute
[1-24].hour
[1-31].day
[1-12].month
jan feb mar apr may jun jul aug sep oct nov dec
and all of those full month names(case insensitive)
sunday, monday, tuesday, wednesday, thursday, friday, saturday
weekday, weekend (case insensitive)
[1].year
当然你也可以用原始的crontab的时间表示方法来表示,如下:
job = Job('demo', every='1,2 5,6 * * 3,4')
其中还可以指定一些特殊的值,如果指定这些值则at参数将会被忽略:
"yearly" # Run once a year at midnight on the morning of January 1
"monthly" # Run once a month at midnight on the morning of the first day
# of the month
"weekly" # Run once a week at midnight on Sunday morning
"daily" # Run once a day at midnight
"hourly" # Run once an hour at the beginning of the hour
"reboot" # Run at startup
at参数则是指定于合适的时间运行,具体实例
job = Job('onejob', every='1.day', at='hour.12 minute.15 minute.45')
# or even better
job = Job('onejob', every='1.day', at='12:15 12:45')
path指定任务(文件)所在路径
environment则预设系统环境变量
output指定标准输出文件名,这里介绍其中最直接的一种写法
job = Job('job', every='1.day', output=dict(stdout='/tmp/stdout.log',stderr='/tmp/stderr.log'))`
自定义Job
总体来说command,script, module命令都是Plan库预设的一些Job,这些Job已经符合我们大多数需求了,如果我们由于某些特殊需求需要设定一些自己的Job,可以参照Plan库设置的这三个Job来进行设置,其中Script源码如下:
class ScriptJob(Job):
"""The script job.
"""
def task_template(self):
"""Template::
'cd {path} && {environment} %s {task} {output}' % sys.executable
"""
return 'cd {path} && {environment} %s {task} {output}' % sys.executable
可见,定义一个新的Job实在是有些简单,只需要将Job类中的task_template
方法重写即可。
Sched
其实这个库非常简单,就几个方法,其中我们需要用到的方法只有两个,enter
, run
实例如下:
>>> import sched, time
>>> s = sched.scheduler(time.time, time.sleep)
>>> def print_time(): print "From print_time", time.time()
...
>>> def print_some_times():
... print time.time()
... s.enter(5, 1, print_time, ())
... s.enter(10, 1, print_time, ())
... s.run()
... print time.time()
...
>>> print_some_times()
930343690.257
From print_time 930343695.274
From print_time 930343700.273
930343700.276
这里我们可以看出这玩意其实只是指定某个时间运行某种方法,但是如果我们变一下同样可以周期运行:
import sched, time
s = sched.scheduler(time.time, time.sleep)
def print_time():
s.enter(5, 1, print_time, ())
print "From print_time", time.time()
def print_some_times():
print time.time()
s.enter(5, 1, print_time, ())
s.run()
print time.time()
这样就形成了一个周期循环的函数,其实这种方法与我们常用的死循环相同
import time
def loop():
_t = 1
while True:
time.sleep(1)
_t += 1
if _t >= 5:
print "time out"
break
test()
def test():
print "get"
loop()
test()
而其实sched其实也与这段代码有些类似,下面是它的部分源码:
class scheduler:
def __init__(self, timefunc, delayfunc):
"""Initialize a new instance, passing the time and delay
functions"""
self._queue = [] # 需要运行的事件
self.timefunc = timefunc
self.delayfunc = delayfunc
def enterabs(self, time, priority, action, argument):
"""Enter a new event in the queue at an absolute time.
Returns an ID for the event which can be used to remove it,
if necessary.
"""
event = Event(time, priority, action, argument)
heapq.heappush(self._queue, event)
return event # The ID
def enter(self, delay, priority, action, argument):
"""A variant that specifies the time as a relative time.
This is actually the more commonly used interface.
"""
time = self.timefunc() + delay
return self.enterabs(time, priority, action, argument)
def run(self):
# localize variable access to minimize overhead
# and to improve thread safety
q = self._queue
delayfunc = self.delayfunc
timefunc = self.timefunc
pop = heapq.heappop
while q:
time, priority, action, argument = checked_event = q[0]
now = timefunc()
if now < time:
delayfunc(time - now) # 相当于死循环中的sleep
else:
event = pop(q)
# Verify that the event was not removed or altered
# by another thread after we last looked at q[0].
if event is checked_event:
action(*argument)
delayfunc(0) # Let other threads run
else:
heapq.heappush(q, event)
不过我们也从源码中看出,这个库针对单线程的还好,针对多线程的库来说程序是极有可能出错的(原因之一它是通过是否含有queue来确定是否继续运行)。
如果是多线程的话推荐使用threading.Timer库来做定时任务。这里贴个实例就不详细介绍了:
>>> import time
>>> from threading import Timer
>>> def print_time():
... print "From print_time", time.time()
...
>>> def print_some_times():
... print time.time()
... Timer(5, print_time, ()).start()
... Timer(10, print_time, ()).start()
... time.sleep(11) # sleep while time-delay events execute
... print time.time()
...
>>> print_some_times()
930343690.257
From print_time 930343695.274
From print_time 930343700.273
930343701.301
Advanced Python Scheduler
这个库说实话真是满足了各种需求,在这三个用法中我最后实践选择的就是这个,符合各种需求,多种方式运行,还可以多种方式来持久化运行记录,如果真要了解它的详细用法估计不得不读一下他那比较多的官方文档,这里只能是介绍常用用法。
安装
pip install apscheduler
#!/usr/bin/env python
# coding=utf-8
import os
from apscheduler.schedulers.blocking import BlockingScheduler
from etc import config
from scripts import auto_script
BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
LOOP_TIME = 3 # 3.day
MYSQL_URL = "mysql://{mysql_user}:{mysql_password}@{mysql_host}/{mysql_dbname}".format(
mysql_user=config.mysql_user,
mysql_password=config.mysql_password,
mysql_host=config.mysql_host,
mysql_dbname=config.job_store_db_name
)
if __name__ == "__main__":
sched = BlockingScheduler()
sched.add_jobstore("sqlalchemy", url=MYSQL_URL)
sched.add_job(func=auto_script.run, trigger="interval", days=LOOP_TIME)
sched.start()
简单来介绍一下:
- 这里先是创建了一个阻塞类型的Scheduler
- 并使用Mysql作为其持久化数据库用来保存期运行记录,其中也非使用的是完全的Mysql官方提供的库,而是使用了Python中最常用的ORM库SqlAlchemy
- 添加了一个job,其指定运行auto_script脚本中的run方法,运行方式(�
trigger
)是间隔时间,间隔时间为LOOP_TIME
Scheduler类型
BlockingScheduler # 在前端运行
BackgroundScheduler # 作为后台进程运行
AsyncIOScheduler # 如果程序中使用了asyncio模块,可使用这个来运行
GeventScheduler # 如果程序中使用了gevent模块,可使用这个来运行
TornadoScheduler # 如果是构建一个Tornado应用,可使用这个来运行
TwistedScheduler # 如果是构建一个Twisted应用,可使用这个来运行
QtScheduler # 如果是构建一个Qt应用,可使用这个来运行
triggers
apscheduler.triggers.cron: 与Cron指定执行方式相同,同时也可指定开始与结束时间
class apscheduler.triggers.cron.CronTrigger(year=None, month=None, day=None, week=None, day_of_week=None, hour=None, minute=None, second=None, start_date=None, end_date=None, timezone=None)
apscheduler.triggers.interval: 间隔时间周期执行,可指定开始与结束时间
class apscheduler.triggers.interval.IntervalTrigger(weeks=0, days=0, hours=0, minutes=0, seconds=0, start_date=None, end_date=None, timezone=None)
apscheduler.triggers.date: 特定时间执行一次
class apscheduler.triggers.date.DateTrigger(run_date=None, timezone=None)
通过 add_job
方法可指定其运行周期方式,而其中参数也由add_job来指定,从其源码来看该方法开头:
def add_job(self, func, trigger=None, args=None, kwargs=None, id=None, name=None,
misfire_grace_time=undefined, coalesce=undefined, max_instances=undefined,
next_run_time=undefined, jobstore='default', executor='default',
replace_existing=False, **trigger_args):
job_kwargs = {
'trigger': self._create_trigger(trigger, trigger_args), # 在此处指定对应的trigger参数
'executor': executor,
'func': func,
'args': tuple(args) if args is not None else (),
'kwargs': dict(kwargs) if kwargs is not None else {},
'id': id,
'name': name,
'misfire_grace_time': misfire_grace_time,
'coalesce': coalesce,
'max_instances': max_instances,
'next_run_time': next_run_time
}
job_kwargs = dict((key, value) for key, value in six.iteritems(job_kwargs) if
value is not undefined)
job = Job(self, **job_kwargs)
# Don't really add jobs to job stores before the scheduler is up and running
with self._jobstores_lock:
if not self.running:
self._pending_jobs.append((job, jobstore, replace_existing))
self._logger.info('Adding job tentatively -- it will be properly scheduled when '
'the scheduler starts')
else:
self._real_add_job(job, jobstore, replace_existing, True)
return job
Job Store
通过add_jobstore(jobstore, alias='default', **jobstore_opts)
指定对应的持久化数据库
可添加的有Mongo, Sqlalchemy类(几乎所有常用关系型数据库), Redis
同时还可以通过在创建Scheduler
实例时候来指定Store
...
jobstores = {
'mongo': MongoDBJobStore(),
'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
...
scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
...
scheduler = BackgroundScheduler({
'apscheduler.jobstores.mongo': {
'type': 'mongodb'
},
'apscheduler.jobstores.default': {
'type': 'sqlalchemy',
'url': 'sqlite:///jobs.sqlite'
},
'apscheduler.executors.default': {
'class': 'apscheduler.executors.pool:ThreadPoolExecutor',
'max_workers': '20'
},
'apscheduler.executors.processpool': {
'type': 'processpool',
'max_workers': '5'
},
'apscheduler.job_defaults.coalesce': 'false',
'apscheduler.job_defaults.max_instances': '3',
'apscheduler.timezone': 'UTC',
})
...
jobstores = {
'mongo': {'type': 'mongodb'},
'default': SQLAlchemyJobStore(url='sqlite:///jobs.sqlite')
}
scheduler = BackgroundScheduler()
scheduler.configure(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)
在这里我们也可以看到,还可以指定executors
已指定线程运行方式,具体可以参考官方文档,这里就不详细展开了。
运行
scheduler.start()
总结
如果是在工作中使用,在只是需要一个定时任务且不复杂的情况下可以使用sched
模块,如果是任务复制则可以使用Advanced Python Scheduler
,如果是一些可以详细拆分的文件定时运行,或者是说定时运行的不是Python脚本则可以使用Plan
最后,水平有限,如果有错误希望能指正并发送邮件与我联系并交流,谢谢。
HashMap源码解析
// 常量列表
DEFAULT_INITIAL_CAPACITY = 1 << 4; //The capacity is the number of buckets in the hash table
MAXIMUM_CAPACITY = 1 << 30;
/*
**The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed (that is, internal data structures are rebuilt) so that the hash table has approximately twice the number of buckets.
*/
DEFAULT_LOAD_FACTOR = 0.75f; //填充因子
put
方法解析:
- 判断是否是空 table ,初始化
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
- 如果key == null 则通过遍历来设置其中的值,可知null不参与hash
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
- 进行hash运算
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k); //猜测是为字符串做了优化提供的Hash,并没有追踪到里面的源代码
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
- 通过hash值与table的长度获取其在数组中的index
return h & (length-1);
当 length 总是 2 的倍数时,h & (length-1)将是一个非常巧妙的设计:假设 h=5, length=16, 那么 h & length - 1 将得到 5;如果 h=6, length=16, 那么 h & length - 1 将得到 6 ……如果 h=15,length=16, 那么 h & length - 1 将得到 15;但是当 h=16 时 , length=16 时,那么 h & length - 1 将得到 0 了;当 h=17 时 , length=16 时,那么 h & length - 1 将得到 1 了……这样保证计算得到的索引值总是位于 table 数组的索引之内。`
- 替换老旧的值,这里循环的目的是为了避免冲突,这里避免冲突的策略是依靠 Entry 这张链表(看其e.next)。
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
- 添加计数
modCount++;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
- 如果key不存在则
addEntry(hash, key, value, i);
return null;
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) { //是否需要重新定义table的大小
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;//计数length
}
get
方法解析
-
取 key = null 的值是直接遍历
-
获取值
Entry<K,V> entry = getEntry(key); return null == entry ? null : entry.getValue(); final Entry<K,V> getEntry(Object key) { if (size == 0) { return null; } int hash = (key == null) ? 0 : hash(key); // 获取hash的key for (Entry<K,V> e = table[indexFor(hash, table.length)]; //避免冲突 e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }
Table
扩充方案
if ((size >= threshold) && (null != table[bucketIndex])) { //是否需要重新定义table的大小
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
在 addEntry 中有这一串代码,其中的 resize
方法是作为
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable, initHashSeedAsNeeded(newCapacity));
table = newTable;
threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
HashMap的并发问题
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next; // (1)
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
//将e放在冲突链表最前面
e.next = newTable[i]; // (2)
newTable[i] = e; // (3)
e = next; // (4)
}
}
}
现在假设我们的oldTable.length = 2, 而 newTable.length = 4。
原有的oldTable的值为 3, 7, 5. 则这三个值是以链表的形式存在于oldTable[1]中的。
此时执行函数 transfer, 如果是单线程程序的话则结果变为 newTable[1]=5->null, newTable[3] = 7->3->null。
如果是多线程,我们假定有一个线程一,一个线程二:
- 线程一执行到代码中的(1)位置进行调度停止,则此时 e = 3, next = 7
- 线程二执行完成,则此时的newTable[1] = 5->null, newTable[3] = 7->3->null
- 此时线程一启动:
- 在 (2) e.next = newTable[3] = 7 -> 3 -> null.
- 在 (3) 的时候 newTable[3] = 3 -> null.
- 在 (4) 的时候 e = next = 7 -> 3
- 此时会形成一个循环 e = 3, e.next = 7, e.next.next = 3 = e
参考文档
ConcurrentHashMap源码解析
ConcurrentMap接口说明
public interface ConcurrentMap<K, V> extends Map<K, V> {
V putIfAbsent(K key, V value); // 如果指定key没有与任何值绑定,则它与value绑定
/* 等同于
if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
*/
boolean remove(Object key, Object value); // 只有当该key与value相映射时候删除掉该元素
/* 等同于
if (map.containsKey(key) && map.get(key).equals(value)) {
map.remove(key);
return true;
} else return false;
*/
boolean replace(K key, V oldValue, V newValue); // 当key的值为oldValue时用newValue代替
/* 等同于
if (map.containsKey(key) && map.get(key).equals(oldValue)) {
map.put(key, newValue);
return true;
} else return false;
*/
V replace(K key, V value); // 当map中包含有key时用value代替
/* 等同于
if (map.containsKey(key)) {
return map.put(key, value);
} else return null;
*/
}
注意:
- ConcurrentMap 中不能存放值null
- 配合java.util.concurrent.atomic使用
为什么不使用 HashTable
HashTable
容器使用synchronized
来保证线程安全,但在线程竞争激烈的情况下HashTable
的效率非常低下。因为当一个线程访问HashTable
的同步方法时,其他线程访问HashTable
的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put
进行添加元素,线程2不但不能使用put
方法添加元素,并且也不能使用get
方法来获取元素,所以竞争越激烈效率越低。
锁分段技术
HashTable
容器在竞争激烈的并发环境下表现出效率低下的原因是所有访问HashTable
的线程都必须竞争同一把锁,那假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap
所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap的结构
ConcurrentHashMap
是由Segment
数组结构和HashEntry
数组结构组成。Segment
是一种可重入锁ReentrantLock
,在ConcurrentHashMap
里扮演锁的角色,HashEntry
则用于存储键值对数据。一个ConcurrentHashMap
里包含一个Segment
数组,Segment
的结构和HashMap
类似,是一种数组和链表结构, 一个Segment
里包含一个HashEntry
数组,每个HashEntry
是一个链表结构的元素, 每个Segment
守护者一个HashEntry
数组里的元素,当对HashEntry
数组的数据进行修改时,必须首先获得它对应的Segment
锁。
ReentrantLock介绍
ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
这里提到一个锁获取的公平性问题,如果在绝对时间上,先对锁进行获取的请求一定被先满足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也可以说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,能够控制这个锁是否是公平的。
而锁的名字也是说明了这个锁具备了重复进入的可能,也就是说能够让当前线程多次的进行对锁的获取操作,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。
nonfairTryAcquire
是非公平的获取锁的方式final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); // 如果当前状态为初始状态,那就尝试设置状态 if (c == 0) { if (compareAndSetState(0, acquires)) { // 状态设置成功就返回 setExclusiveOwnerThread(current); return true; } } // 如果状态被设置,且获取锁的线程又是当前线程的时候,进行状态的自增; else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 如果未设置成功状态且当前线程不是获取锁的线程,那么返回失败。 return false; }
初始化
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// hash算法中需要使用
this.segmentShift = 32 - sshift; // 用于定位参与hash运算的位数
this.segmentMask = ssize - 1; // hash运算的掩码
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
通过concurrencyLevel
(默认是16)得出Segment
数组的长度,为了能通过按位与的哈希算法来定位segments
数组的索引,必须保证segments
数组的长度是2的N次方,所以必须计算出一个是大于或等于concurrencyLevel
的最好的2得N次方值来作为segments
数组的长度。假设为14/15/16,则ssize都为16,即容器里锁的个数也为16。
transient关键字
这个字段的生命周期仅存于调用者的内存中而不会写到磁盘里持久化。
简单来说就是当一个对象需要序列化时,这个方法用于修饰不需要被序列化的字段。
putIfAbsent
public V putIfAbsent(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
// sun.misc.unsafe类中使用getObject来检测实际值的字段
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null)
// 如果不存在Segment,则创建一个sengment然后再插入HashEntry
s = ensureSegment(j);
return s.put(key, hash, value, true);
}
ensureSegment方法
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
// getObjectVolatile是以Volatile的方式获得目标的Segment,Volatile是为了保证可见性。
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
// 不存在该segment则需要重新创建,其中是以ss[0]为镜像进行创建
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
// 重复检测
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
// 创建新的Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
//以CAS的方式,将新建的Segment,set到指定的位置。
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术。简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
Segment的put方法
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试获取锁,如果能获取锁则根据hash的地址获取其HashEntry实例
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table; // table是segment中独有的table
int index = (tab.length - 1) & hash;
/*
通过位运算找出HashEntry数组中index位置的值,
这里的位运算运用到了UNSAFE中的方法。
*/
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
// 该位置是否有可替代的值
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
// 这儿是 ConcurrentMap的put方法和putIfAbsent方法的区别
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
if (node != null)
// 通过链表来解决冲突
// 这个地方实现就是粗暴的 UNSAFE.putOrderedObject(this, nextOffset, n);
node.setNext(first);
else
// 没有冲突则创建一个新的 HashEntry
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 查看是否需要更新HashEntry
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 重置HashEntry的位置
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
// 解锁
unlock();
}
return oldValue;
}
Segment中的scanAndLockForPut
方法
/**
* Scans for a node containing given key while trying to
* acquire lock, creating and returning one if not found. Upon
* return, guarantees that lock is held. UNlike in most
* methods, calls to method equals are not screened: Since
* traversal speed doesn't matter, we might as well help warm
* up the associated code and accesses as well.
*
* @return a new node if key not found, else null
*/
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
Segment的rehash
方法
private void rehash(HashEntry<K,V> node) {
/*
* Reclassify nodes in each list to new table. Because we
* are using power-of-two expansion, the elements from
* each bin must either stay at same index, or move with a
* power of two offset. We eliminate unnecessary node
* creation by catching cases where old nodes can be
* reused because their next fields won't change.
* Statistically, at the default threshold, only about
* one-sixth of them need cloning when a table
* doubles. The nodes they replace will be garbage
* collectable as soon as they are no longer referenced by
* any reader thread that may be in the midst of
* concurrently traversing table. Entry accesses use plain
* array indexing because they are followed by volatile
* table write.
*/
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
// 遍历旧的HashEntry[]填充到新的里面去, 由于之前的trylock()操作所以在该Segment不需要担心并发问题
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
get
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
// 由于Segment的table是volatile的,所以不需要加锁来获取
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
Volatile 关键字
在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
remove
public boolean remove(Object key, Object value) {
int hash = hash(key);
Segment<K,V> s;
return value != null && (s = segmentForHash(hash)) != null &&
s.remove(key, hash, value) != null;
}
Segment的remove
方法
final V remove(Object key, int hash, Object value) {
if (!tryLock()) //尝试获取锁,有次数限制的
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
//删除流程
HashEntry<K,V> next = e.next;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
//如果没有冲突重新设置next的位置
setEntryAt(tab, index, next);
else
// 确定前面的节点,然后前面节点连接被删除节点的next节点
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
Segment的scanAndLock
/**
* Scans for a node containing the given key while trying to
* acquire lock for a remove or replace operation. Upon
* return, guarantees that lock is held. Note that we must
* lock even if the key is not found, to ensure sequential
* consistency of updates.
*/
private void scanAndLock(Object key, int hash) {
// similar to but simpler than scanAndLockForPut
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
int retries = -1;
while (!tryLock()) {
HashEntry<K,V> f;
if (retries < 0) {
if (e == null || key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f;
retries = -1;
}
}
}
replace(K key, V value)
public V replace(K key, V value) {
int hash = hash(key);
if (value == null)
throw new NullPointerException();
Segment<K,V> s = segmentForHash(hash);
return s == null ? null : s.replace(key, hash, value);
}
Segment的V replace(K key, int hash, V value)
final V replace(K key, int hash, V value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V> e;
for (e = entryForHash(this, hash); e != null; e = e.next) { //只有key才能replace
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
e.value = value;
++modCount;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
replace(K key, V oldValue, V newValue)
public boolean replace(K key, V oldValue, V newValue) {
int hash = hash(key);
if (oldValue == null || newValue == null)
throw new NullPointerException();
Segment<K,V> s = segmentForHash(hash);
return s != null && s.replace(key, hash, oldValue, newValue);
}
基本同上
sun.misc.Unsafe
可以有意的执行一些不安全、容易犯错的操作。
其使用场景:
- 对变量和数组内容的原子访问,自定义内存屏障
- 对序列化的支持
- 自定义内存管理/高效的内存布局
- 与原生代码和其他JVM进行互操作
- 对高级锁的支持
参考文档
JVM系列—虚拟机类加载机制
虚拟机类加载机制
概述
什么是类加载
虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的类加载机制。
为什么有类加载
Class 文件中所描述的信息,是使用字节码进行描述的,而这种描述必然是机器无法读取的,需要 JVM 在中间作用来将其转换成机器可以运行的信息。而且对于 Java 语言来说,类型的加载,连接和初始化过程都在程序运行期间完成,这虽然会令类加载时稍微增加一些性能上的开销,但是所带来的是 Java 高于 C 这类语言的灵活度,Java 里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
类加载发生在什么时候
类加载的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如下图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。注意,这里笔者写的是按部就班地『开始』,而不是按部就班地“进行”或“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
类加载的时机
什么情况下需要开始类加载过程的第一个阶段加载?Java虚拟机规范中并没有进行强制约束,也就是说不同虚拟机上面实现可能是不一样的,这个由虚拟机的具体实现来自由把握。
类初始化的时机
对于初始化阶段,虚拟机规范则是严格规定了有且只有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)。
特定字节码指令
遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这 4 条指令的最常见的 Java 代码场景是:使用 new 关键字实例化对象的时候、读取或设置一个类的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- new: 创建一个对象,并将其引入值压入栈顶
- getstatic: 获取指定类的静态域,并将其值压入栈顶
- putstatic: 为指定的类的静态域赋值
- invokestatic: 调用静态方法
反射
使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
初始化子类其父类未被初始化
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
用户指定的主类
当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()
方法的那个类),虚拟机会先初始化这个主类。
JDK1.7 动态语言支持
当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle
实例最后的解析结果是 REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
被动引用
上述 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用,接下来举三个被动引用的例子。
package io.github.binglau.jvm.passive_reference;
class SuperClass {
static {
System.out.println("SuperClass init!");
}
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class NotInitialization {
public static void main(String[] args) {
// 通过子类引用父类的静态字段,不会导致子类初始化
// 只会输出 SuperClass init! 而不会输出 SubClass init!
System.out.println(SubClass.value); // 1
// 通过数组定义来引用类,不会触发此类的初始化
// 没有输出
SuperClass[] sca = new SuperClass[10]; // 2
// 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,
// 因此不会触发定义常量的类的初始化
// 因为在编译阶段通过常量传播优化,已经将此常量值 "hello world" 存储到
// NotInitialization 类的常量池中,以后 NotInitialization 对常量
// ConstClass.HELLOWORLD 的引用都被转化为 NotInitialization 类对自身常量池的引用
System.out.println(ConstClass.HELLOWORLD); // 3
}
}
/**
结果:
# 1 输出结果
SuperClass init!
123
# 3 输出结果
hello world
**/
扩展:至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。使用Sun HotSpot虚拟机通过-XX:+TraceClassLoading参数可观察到此操作会导致子类的加载(但未初始化)。
.... [Loaded NotInitialization from file:/Users/binglau/code/cradle/Java-demo/basic/src/main/java/io/github/binglau/jvm/passive_reference/] [Loaded sun.launcher.LauncherHelper$FXHelper from /Library/Java/JavaVirtualMachines/jdk8/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Class$MethodArray from /Library/Java/JavaVirtualMachines/jdk8/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Void from /Library/Java/JavaVirtualMachines/jdk8/Contents/Home/jre/lib/rt.jar] [Loaded SuperClass from file:/Users/binglau/code/cradle/Java-demo/basic/src/main/java/io/github/binglau/jvm/passive_reference/] [Loaded SubClass from file:/Users/binglau/code/cradle/Java-demo/basic/src/main/java/io/github/binglau/jvm/passive_reference/] SuperClass init 123 hello world [Loaded java.lang.Shutdown from /Library/Java/JavaVirtualMachines/jdk8/Contents/Home/jre/lib/rt.jar] [Loaded java.lang.Shutdown$Lock from /Library/Java/JavaVirtualMachines/jdk8/Contents/Home/jre/lib/rt.jar]
上述输入皆没有加载相应的类
类加载过程
加载
需要做的事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
其规定并非具体,而是根据各个虚拟机实现来定义的,比如『通过一个类的全限定名来获取定义此类的二进制字节流』这条,并没有指定非要从一个 class 文件中获取二进制字节流,有很多别样的玩法,比如:
- 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
- 从网络中获取,这种场景最典型的应用就是Applet。
- 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
- 由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。
- 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
- ….
加载类与数组
相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()
方法)。
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有维度的类型)最终是要靠类加载器去创建,一个数组类(下面简称为C)创建过程就遵循以下规则:
- 如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组 C 将在加载该组件类型的类加载器的类名称空间上被标识(这点很重要,后面会介,一个类必须与类加载器一起确定唯一性)。
- 如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组 C 标记为与引导类加载器关联。
- 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
验证
目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致上会完成下面 4 个阶段的验证工作:
文件格式验证
本阶段要验证字节流是否符合 Class 文件格式的规范,是否能被当前版本的虚拟机处理。主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java 类型信息的要求。本阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的 3 个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
包括但不限于:
- 是否以魔数0xCAFEBABE开头。
- 主、次版本号是否在当前虚拟机处理范围之内。
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
CONSTANT_Utf8_info
型的常量中是否有不符合UTF8编码的数据。- Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。
元数据验证
本阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
可能包括但不限于:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
字节码验证
本阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在元数据验证阶段对元数据信息中的数据类型做完校验后,本阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。例如:
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的。可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。
别相信机器给你纠错,不然他就先给自己纠错了。
符合引用验证
本阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验下列内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError
异常的子类,如java.lang.IllegalAccessError
、java.lang.NoSuchFieldError
、java.lang.NoSuchMethodError
等。
对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none
参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
本阶段中有两个容易产生混淆的概念需要强调一下:
- 此时进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
- 这里所说的初始值“通常情况”下是数据类型的零值。
通常情况
public static int value = 123;
这里的 value 在准备之后的值是 0 而不是 123
特殊情况
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在前一章讲解Class文件格式的时候已经出现过多次,在 Class 文件中它以 CONSTANT_Class_info
、CONSTANT_Fieldref_info
、CONSTANT_Methodref_info
等类型的常量出现。那解析阶段中所说的直接引用与符号引用又有什么关联呢?
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
- 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
虚拟机规范之中并未规定解析阶段发生的具体时间,只要求了在执行anewarray
、checkcast
、getfield
、getstatic
、instanceof
、invokedynamic
、invokeinterface
、invokespecial
、invokestatic
、invokevirtual
、ldc
、ldc_w
、multianewarray
、new
、putfield
和 putstatic
这16个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
符号解析中的差异
对同一个符号引用进行多次解析请求是很常见的事情,**除invokedynamic
指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。**无论是否真正执行了多次解析动作,虚拟机需要保证的是在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一直成功;同样的,如果第一次解析失败了,那么其他指令对这个符号的解析请求也应该收到相同的异常。
对于invokedynamic
指令,上面规则则不成立。当碰到某个前面已经由invokedynamic
指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic
指令也同样生效。因为invokedynamic
指令的目的本来就是用于动态语言支持(目前仅使用Java语言不会生成这条字节码指令),它所对应的引用称为『动态调用点限定符』(Dynamic Call Site Specifier),这里『动态』的含义就是必须等到程序实际运行到这条指令的时候,解析动作才能进行。相对的,其余可触发解析的指令都是“静态”的,可以在刚刚完成加载阶段,还没有开始执行代码时就进行解析。
解析对象
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,分别对应于常量池的 CONSTANT_Class_info
、CONSTANT_Field-ref_info
、CONSTANT_Methodref_info
、CONSTANT_InterfaceMethodref_info
、CONSTANT_MethodType_info
、CONSTANT_MethodHandle_info
和 CONSTANT_InvokeDynamic_info
7种常量类型
类或接口的解析
假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析的过程需要以下3个步骤:
- **如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。**在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败。
- 如果C是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需要加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
- **如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。**如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。
字段解析
要解析一个未被解析过的字段符号引用,首先将会对字段表内 class_index
项中索引的CONSTANT_Class_info
符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。
- 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束(类本身字段描述)
- 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。(接口及接口继承)
- 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。(类继承)
- 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
- 如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常。
类方法解析
类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index
项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。
- 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。(类本身方法)
- 如果通过了第1步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
- 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。(类继承父类的方法)
- 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出
java.lang.AbstractMethodError
异常。(抽象类查找抽象方法) - 否则,宣告方法查找失败,抛出
java.lang.NoSuchMethodError
。 - 如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出
java.lang.IllegalAccessErro
r异常。
接口方法解析
接口方法也需要先解析出接口方法表的class_index
项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索。
- 与类方法解析不同,如果在接口方法表中发现
class_index
中的索引C是个类而不是接口,那就直接抛出java.lang.IncompatibleClassChangeError
异常。(接口确认) - 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。(查找接口本身方法)
- 否则,在接口C的父接口中递归查找,直到
java.lang.Object
类(查找范围会包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。(查找继承接口方法) - 否则,宣告方法查找失败,抛出
java.lang.NoSuchMethodError
异常。
由于接口中的所有方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError
异常。
初始化
初始化之前除了用户可以自定义类加载器参与之前都是由 JVM 主导和控制,到了初始化阶段,才真正开始执行类中定义的 Java 程序代码(或者说是字节码)
在准备阶段,类变量已经赋过一次系统要求的初始值。在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。
<client>()
方法
<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}
块)中的语句合并产生。编译器收集的顺序由语句在源文件中出现的顺序所决定。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,示例代码如下所示。
package io.github.binglau.jvm;
public class TestClassinitialization {
static {
i = 0;//给变量赋值可以正常编译通过
System.out.print(i);//这句编译器会提示"非法向前引用"
}
static int i = 1;
}
<clinit>()
执行特定和细节
这里只限于 Java 语言编译产生的 Class 文件,并不包括其他 JVM 语言。
-
<clinit>()
方法与类的构造函数(或者说实例构造器<init>()
方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()
方法执行之前,父类的<clinit>()
方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()
方法的类肯定是java.lang.Object。 -
<clinit>()
方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()
方法。 -
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此**接口与类一样都会生成
<clinit>()
方法。但接口与类不同的是,执行接口的<clinit>()
方法不需要先执行父接口的<clinit>()
方法。只有当父接口中定义的变量使用时,父接口才会初始化。**另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()
方法。 -
虚拟机会保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()
方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()
方法完毕。如果在一个类的<clinit>()
方法中有耗时很长的操作,就可能造成多个进程阻塞(被唤醒之后不会再次进入<clinit>()
方法,同一个类加载器下一个类型只会初始化一次),在实际应用中这种阻塞往往是很隐蔽的。package io.github.binglau.jvm; /** * 文件描述: */ public class TestClassinitialization { static class DeadLoopClass { static { /*如果不加上这个if语句,编译器将提示"Initializer does not complete normally"并拒绝编译*/ if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } public static void main(String[] args) { Runnable script = new Runnable() { public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + "run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); } } /** 运行结果如下,即一条线程在死循环以模拟长时间操作,另外一条线程在阻塞等待 Thread[Thread-1,5,main]start Thread[Thread-0,5,main]start Thread[Thread-1,5,main]init DeadLoopClass **/
参考书籍
《深入理解 Java 虚拟机》
ElasticSearch进阶
索引
映射
简而言之,映射即为结构(虽然说 ElasticSearch 是一个无模式的搜索引擎)。
定义方式:
curl -XPUT 'localhost:9200/my_index?pretty' -H 'Content-Type: application/json' -d'
{
"mappings": {
"doc": {
"properties": { // 字段定义
"title": { "type": "text" },
"name": { "type": "text" },
"age": { "type": "integer" },
"created": {
"type": "date", // 类型定义
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}
}
'
核心类型
每个字段类型可以指定为 Elasticsearch 提供的一个特定核心类型。Elasticsearch 有以下核心类型:
- string:字符串。
text
与keyword
- number: 数字。
long
,integer
,short
,byte
,double
,float
,half_float
,scaled_float
- date:日期。
date
- boolean:布尔型。
boolean
- binary:二进制。
binary
- range: 范围值。
integer_range
,float_range
,long_range
,double_range
,date_range
除了核心类型之外还有复杂的数组,对象,内嵌对象类型,地理位置,特有类型。
公共属性
- index_name:该属性定义将存储在索引中的字段名称。若未定义,字段将以对象的名字来命名。
- index:可设置值为 analyzed 和 no。另外,对基于字符串的字段,也可以设置为 not_analyzed。如果设置为analyzed,该字段将被编入索引以供搜索。如果设置为 no,将无法搜索该字段。默认值为 analyzed。在基于字符串的字段中,还有一个额外的选项 not_analyzed。此设置意味着字段将不经分析而编入索引,使用原始值被编入索引,在搜索的过程中必须全部匹配。索引属性设置为 no 将使 include_in_all 属性失效。
- store:这个属性的值可以是 yes 或 no,指定了该字段的原始值是否被写入索引中。默认值设置为no,这意味着在结果中不能返回该字段(然而,如果你使用_source字段,即使没有存储也可返回返回这个值),但是如果该值编入索引,仍可以基于它来搜索数据。_
- boost:该属性的默认值是1。基本上,它定义了在文档中该字段的重要性。boost 的值越高,字段中值的重要性也越高。
- null_value:如果该字段并非索引文档的一部分,此属性指定应写入索引的值。默认的行为是忽略该字段。_
- copy_to:此属性指定一个字段,字段的所有值都将复制到该指定字段。_
- include_in_all:此属性指定该字段是否应包括在_all字段中。默认情况下,如果使用_all字段,所有字段都会包括
具体深入到各个类型特有属性值,请参考官方文档,这里就不一一指出了。
使用分析器
对于字符串类型的字段,可以指定 Elasticsearch 应该使用哪个分析器。分析器是一个用于分析数据或以我们想要的方式查询数据的工具。例如,用空格和小写字符把单词隔开时,不必担心用户发送的单词是小写还是大写。Elasticsearch 使我们能够在索引和查询时使用不同的分析器,并且可以在搜索过程的每个阶段选择处理数据的方式。使用分析器时,只需在指定字段的正确属性上设置它的名字,就这么简单。
开箱机用的分析器
Elasticsearch 允许我们使用众多默认定义的分析器中的一种。如下分析器可以开箱即用。
-
方便大多数欧洲语言的标准分析器。
-
基于非字母字符来分离所提供的值,并将其转换为小写形式。
-
基于空格
-
类似于 Simple,除了 Simple 提供的之外,还基于提供的停用词过滤数据
-
非常简单的分析器,只传入提供的值。你可以通过指定字段为
not_analyzed
来达到相同的目的。 -
通过正则来分离文本
-
特定语言环境下工作,支持多种语言。
-
基于自己创建的一个检测重复的 fingerprint 分析器。
定义自己的分析器
通过简单设置过滤器来定义自己的分析器
PUT my_index
{
"settings": {
"analysis": {
"analyzer": {
"my_custom_analyzer": {
"type": "custom",
"tokenizer": "standard",
"char_filter": [
"html_strip"
],
"filter": [
"lowercase",
"asciifolding"
]
}
}
}
}
}
定义了一个 my_custom_analyzer 的分析器
扩展索引结构
元字段
-
_id
存储着索引时设置的实际标识符
-
_uid
_type
与_id
结合,文档唯一标识符 -
_type
文档类型
-
_all
存储其他字段中的数据以便于搜索。
-
_index
存储文档相关索引信息。
-
_source
可以生成索引过程中存储发送到 ElasticSearch 的原始 JSON 文档。
-
_size
自动索引
_source
字段的原始大小。
嵌套结构
基本上,通过使用嵌套对象,Elasticsearch 允许我们连接一个主文档和多个附属文档。主文档及嵌套文档一同被索引,放置于索引的同一段上(实际在同一块上),确保为该数据结构获取最佳性能。更改文档也是一样的,除非使用更新API,你需要同时索引父文档和其他所有嵌套文档。
PUT my_index/my_type/1
{
"group" : "fans",
"user" : [
{
"first" : "John",
"last" : "Smith"
},
{
"first" : "Alice",
"last" : "White"
}
]
}
GET my_index/_search
{
"query": {
"bool": {
"must": [
{ "match": { "user.first": "Alice" }},
{ "match": { "user.last": "Smith" }}
]
}
}
}
段合并
随着时间的推移和持续索引数据,越来越多的段被创建。因此,搜索性能可能会降低,而且索引可能比原先大,因为它仍含有被删除的文件。这使得段合并有了用武之地。
段合并的处理过程是:底层的 Lucene 库获取若干段,并在这些段信息的基础上创建一个新的段。由此产生的段拥有所有存储在原始段中的文档,除了被标记为删除的那些之外。合并操作之后,源段将从磁盘上删除。这是因为段合并在CPU和I/O的使用方面代价是相当高的,关键是要适当地控制这个过程被调用的时机和频率。
段合并的必要性
- 构成索引的段越多,搜索速度越慢,需要使用的Lucene内存也越多。
- 索引使用的磁盘空间和资源,例如文件描述符。如果从索引中删除许多文档,直到合并发生,则这些文档只是被标记为已删除,而没有在物理上删除。因而,大多数占用了CPU和内存的文档可能并不存在!
好在Elasticsearch使用合理的默认值做段合并,这些默认值很可能不再需要做任何更改。
合并策略
三种策略:
- tiered:这是默认合并策略,合并尺寸大致相似的段,并考虑到每个层(tier)允许的最大段数量;
- log_byte_size:这个合并策略下,随着时间推移,将产生由索引大小的对数构成的索引,其中存在着一些较大的段以及一些合并因子较小的段等;
- log_doc:这个策略类似于log_byte_size合并策略,但根据索引中的文档数而非段的实际字节数来操作。
合并调度器
指示 ElasticSearch 合并过程的方式,有如下可能:
- 并发合并调度器:这是默认的合并过程,在独立的线程中执行,定义好的线程数量可以并行合并。
- 串行合并调度器:这一合并过程在调用线程(即执行索引的线程)中执行。合并进程会一直阻塞线程直到合并完成。调度器可使用index.merge.scheduler.type参数设置。若要使用串行合并调度器,需把参数值设为serial;若要使用并发调度器,则需把参数值设为concurrent。
路由介绍
默认情况下,Elasticsearch 会在所有索引的分片中均匀地分配文档。然而,这并不总是理想情况。为了获得文档,Elasticsearch必须查询所有分片并合并结果。然而,如果你可以把数据按照一定的依据来划分(例如,客户端标识符),就可以使用一个强大的文档和查询分布控制机制:路由。简而言之,它允许选择用于索引和搜索数据的分片。
默认索引过程
默认情况下,Elasticsearch 计算文档标识符的散列值,以此为基础将文档放置于一个可用的主分片上。接着,这些文档被重新分配至副本。
默认搜索过程
一般而言,我们将查询发送到 Elasticsearch 的一个节点,Elasticsearch将会根据搜索类型来执行查询。这通常意味着它首先查询所有节点得到标识符和匹配文档的得分,接着发送一个内部查询,但仅发送到相关的分片(包含所需文档的分片),最后获取所需文档来构建响应。
如下图所示
假使把单个用户的所有文档放置于单个分片之中,并对此分片查询,会出现什么情况?是否对性能来说不明智?不,这种操作是相当便利的,也正是路由所允许的。
路由
要记住,使用路由时,你仍然应该为与路由值相同的值添加一个过滤器。这是因为,路由值的数量或许会比索引分片的数量多。因此,一些不同的属性值可以指向相同的分片,如果你忽略过滤,得到的数据并非是路由的单个值,而是特定分片中驻留的所有路由值。
路由参数
最简单的方法(但并不总是最方便的一个)是使用路由参数来提供路由值。索引或查询时,你可以添加路由参数到HTTP,或使用你所选择的客户端库来设置。
添加到固定路由值中
POST /twitter/tweet?routing=kimchy
{
"user" : "kimchy",
"postDate" : "2009-11-15T14:12:12",
"message" : "trying out Elasticsearch"
}
从路由值中查询
POST /twitter/tweet/_search?routing=kimchy
{
"query": {
"bool" : {
"must" : {
"query_string" : {
"query" : "some query string here"
}
},
"filter" : {
"term" : { "user" : "kimchy" }
}
}
}
}
#####路由字段
为每个发送到Elasticsearch的请求指定路由值并不方便。事实上,在索引过程中,Elasticsearch允许指定一个字段,用该字段的值作为路由值。这样只需要在查询时提供路由参数。为此,在类型定义中需要添加以下代码:
"_routing": {
"required": true,
"path": "userId"
}
上述定义意味着需要提供路由值("required":true
属性),否则,索引请求将失败。除此之外,我们还指定了path属性,说明文档的哪个字段值应被设置为路由值,在上述示例中,我们使用了 userId 字段值。这两个参数意味着用于索引的每个文档都需要定义 userId 字段。
添加路由部分后,整个更新的映射文件将如下所示:
{
"mappings": {
"post": {
"_routing": {
"required": true,
"path": "userId"
},
"properties": {
"id": {"type": "long", "store": "yes", "precision_ step": "0"},
"name": {"type" :"string", "store": "yes", "index": "analyzed"},
"contents": {"type": "string", "store": "no", "index": "analyzed" },
"userId": {"type": "long", "store": "yes", "precision_ step": "0"}
}
}
}
}
如果想使用上述映射来建 post 索引可以这样:
curl -XPOST 'localhost:9200/posts/post/ 1' -d '{
"id": 1,
"name":" New post",
"contents": "New test post",
"userId": 1234567
}'
这样 ElasticSearch 将使用 1234567 作为索引时的路由值。
其他查询
复合查询
复合查询就是支持可以把多个查询连接起来,或者改变其他查询的行为。
布尔查询
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user" : "kimchy" }
},
"filter": {
"term" : { "tag" : "tech" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tag" : "wow" } },
{ "term" : { "tag" : "elasticsearch" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}
可以通过布尔查询来封装无限数量的查询,并通过下面描述的节点之一使用一个逻辑值来连接它们。
- should:被它封装的布尔查询可能被匹配,也可能不被匹配。被匹配的should节点数目由minimum_should_match参数控制。
- must:被它封装的布尔查询必须被匹配,文档才会返回。
- must_not:被它封装的布尔查询必须不被匹配,文档才会返回。
上述每个节点都可以在单个布尔查询中出现多次。这允许建立非常复杂的查询,有多个嵌套级别(在一个布尔查询中包含另一个布尔查询)。记住,结果文档的得分将由文档匹配的所有封装的查询得分总和计算得到。
除了上述部分以外,还可以在查询主体中添加以下参数控制其行为。
- boost:此参数指定了查询使用的加权值,默认为1.0。加权值越高,匹配文档的得分越高。
- minimum_should_match:此参数的值描述了文档被视为匹配时,应该匹配的should子句的最少数量。举例来说,它可以是个整数值,比如2,也可以是个百分比,比如75%。
- disable_coord:此参数的默认值为false,允许启用或禁用分数因子的计算,该计算是基于文档包含的所有查询词条。如果得分不必太精确,但要查询快点,那么应该将它设置为true。
加权查询
GET /_search
{
"query": {
"boosting" : {
"positive" : {
"term" : {
"field1" : "value1"
}
},
"negative" : {
"term" : {
"field2" : "value2"
}
},
"negative_boost" : 0.2
}
}
}
加权查询封装了两个查询,并且降低其中一个查询返回文档的得分。加权查询中有三个节点需要定义:
- positive部分,包含所返回文档得分不会被改变的查询;
- negative部分,返回的文档得分将被降低;
- negative_boost部分,包含用来降低negative部分查询得分的加权值。
加权查询的优点是,positive 部分和 negative 部分包含的查询结果都会出现在搜索结果中,而某些查询的得分将被降低。如果使用布尔查询的 must_not 节点,将得不到这样的结果。
constant_score 查询
GET /_search
{
"query": {
"constant_score" : {
"filter" : {
"term" : { "user" : "kimchy"}
},
"boost" : 1.2
}
}
}
constant_score 查询封装了另一个查询(或过滤),并为每一个所封装查询(或过滤)返回的文档返回一个常量得分。它允许我们严格控制与一个查询或过滤匹配的文档得分。
function_score 查询
GET /_search
{
"query": {
"function_score": {
"query": { "match_all": {} },
"boost": "5",
"random_score": {},
"boost_mode":"multiply"
}
}
}
function_score 查询允许我们通过提供一些计算函数来修改检索文档的得分。
过滤器
后过滤器
出于性能考虑,当你需要对搜索结果和聚合结果做不同的过滤时,你才应该使用 post_filter
, post_filter
的特性是在查询之后 执行,任何过滤对性能带来的好处(比如缓存)都会完全失去。
在我们需要不同过滤时, post_filter
只与聚合一起使用。
在任何搜索中使用过滤器,只需在于 query 节点相同级别上添加一个 filter 节点。如果你只想要过滤器,也可以完全忽略 query 节点。示例,搜索 title 字段并向其添加过滤器:
{
"query": {
"match": {"title": "Catch-22"}
},
"post_filter": {
"term": {"year": 1961}
}
}
其返回结果会缩小到过滤器指定的范围中。
过滤器
绝大部分字段与 query 相同,具体可参照文档。
filter or post_filter
To sum it up
- a
filtered
query affects both search results and aggregations- while a
post_filter
only affects the search results but NOT the aggregations
排序
默认是以 _score
的倒叙排序的
指定字段排序
{
"sort" : [
{ "post_date" : {"order" : "asc"}},
"user",
{ "name" : "desc" },
{ "age" : "desc" },
"_score"
],
"query" : {
"term" : { "user" : "kimchy" }
}
}
类似于指定在 sore
里面的 array 字段。排序数组中的字段,如果字段相同就采用下一个字段来确定顺序。
The order defaults to
desc
when sorting on the_score
, and defaults toasc
when sorting on anything else.
指定缺少字段行为
如果排序字段有缺失可以指定为第一个(_first
)或最后一个(_last
)
{
"sort" : [
{ "price" : {"missing" : "_last"} }
],
"query" : {
"term" : { "product" : "chocolate" }
}
}
动态排序
ElasticSearch 允许我们使用具有多个值得字段进行排序,可以使用脚本来控制排序比较告诉 ElasticSearch 如果计算应用于排序的值来达到目的。
{
"query" : {
"term" : { "user" : "kimchy" }
},
"sort" : {
"_script" : {
"type" : "number",
"script" : {
"lang": "painless",
"source": "doc['field_name'].value * params.factor",
"params" : {
"factor" : 1.1
}
},
"order" : "asc"
}
}
}
Explain
ElasticSearch 的 Explain API 会给出查询的文档的具体计算解释,无论这个文档是否匹配到了这条查询。
GET /twitter/tweet/0/_explain
{
"query" : {
"match" : { "message" : "elasticsearch" }
}
}
{
"_index": "twitter",
"_type": "tweet",
"_id": "0",
"matched": true,
"explanation": {
"value": 1.6943599,
"description": "weight(message:elasticsearch in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 1.6943599,
"description": "score(doc=0,freq=1.0 = termFreq=1.0\n), product of:",
"details": [
{
"value": 1.3862944,
"description": "idf, computed as log(1 + (docCount - docFreq + 0.5) / (docFreq + 0.5)) from:",
"details": [
{
"value": 1.0,
"description": "docFreq",
"details": []
},
{
"value": 5.0,
"description": "docCount",
"details": []
}
]
},
{
"value": 1.2222223,
"description": "tfNorm, computed as (freq * (k1 + 1)) / (freq + k1 * (1 - b + b * fieldLength / avgFieldLength)) from:",
"details": [
{
"value": 1.0,
"description": "termFreq=1.0",
"details": []
},
{
"value": 1.2,
"description": "parameter k1",
"details": []
},
{
"value": 0.75,
"description": "parameter b",
"details": []
},
{
"value": 5.4,
"description": "avgFieldLength",
"details": []
},
{
"value": 3.0,
"description": "fieldLength",
"details": []
}
]
}
]
}
]
}
}
看起来有点复杂,这里最重要的内容就是对文档计算得到的总分,如果总分等于0,则该文档将不能匹配给定的查询。另一个重要内容是关于不同打分项的描述信息。根据查询类型的不同,打分项会以不同方式对最后得分产生影响。
搜索进阶
Apache Lucene 评分简介
TF/IDF 算法的实用计算公式如下:
$$
score(q, d) = coord(q, d) * queryNorm(q) * \sum_{tinq} (tf(tind)*idf(t)^2 * boot(t) * norm(t,d))
$$
其中 q 是查询 d 是文档。还有两个不直接依赖于查询词条的因子 coord 和 queryNorm。
公式中这两个元素跟查询中的每个词计算而得的总和相乘。另一方面,该总和由给定词的词频、逆文档频率、词条加权和规范(长度规范)相乘而来。
上述规则的好处是,你不需要记住全部内容。应该知道的是影响文档评分的因素。下面是一些派生自上述等式的规则
派生规则
- 匹配的词条越罕见,文档的得分越高;
- 文档的字段越小,文档的得分越高;
- 字段的加权越高,文档的得分越高;
- 我们可以看到,文档匹配的查询词条数目越高、字段越少(意味着索引的词条越少),Lucene给文档的分数越高。同时,罕见词条比常见词条更受评分的青睐。
ElasticSearch 脚本功能
Elasticsearch有几个可以使用脚本的功能。你已经看过一些例子,如更新文件、过滤和搜索。
Elasticsearch使用脚本执行的任何请求中,我们会注意到以下相似的属性。
- script: 实际包含的脚本代码
- lang: 定义实用的脚本语言,默认为 mvel
- params: 此对象包含参数及其值。每个定义的参数可以通过指定参数名称在脚本中使用。通过使用参数,我们可以编写更干净的代码。由于可以缓存,使用参数的脚本比嵌入常数的代码执行得更快。
实例:排序的脚本功能
{
"function_score": {
"functions": [
{ ...location clause... },
{ ...price clause... },
{
"script_score": {
"params": {
"threshold": 80,
"discount": 0.1,
"target": 10
},
"script": "price = doc['price'].value; margin = doc['margin'].value;
if (price < threshold) { return price * margin / target };
return price * (1 - discount) * margin / target;"
}
}
]
}
}
脚本执行中可用对象
在不同的操作过程中,Elasticsearch允许在脚本中使用不同的对象。比如,在搜索过程中,下列对象是可用的。
- doc(也可以用doc):这是个
org.elasticsearch.search.lookup.DocLookup
对象的实例。通过它可以访问当前找到的文档,附带计算的得分和字段的值。 - source:这是个
org.elasticsearch.search.lookup.SourceLookup
对象的实例,通过它可以访问当前文档的 source,以及定义在 source 中的值。 - fields:这是个
org.elasticsearch.search.lookup.FieldsLookup
对象的实例,通过它可以访问文档的所有字段。
另一方面,在文档更新过程中,Elasticsearch 只通过 _source 属性公开了 ctx 对象,通过它可以访问当前文档。
我们之前看到过,在文档字段和字段值的上下文中提到了几种方法。现在让我们通过下面的例子,看看如何获取title字段的值。
在括号中,你可以看到 Elasticsearch 从 library 索引中为我们的一个示例文档返回的值:
- doc.title.value(crime);
- source.title(CrimeandPunishment);
- fields.title.value(null)。
有点疑惑,不是吗?在索引期间,一个字段值作为 source 文档的一部分被发送到 Elasticsearch。Elasticsearch 可以存储此信息,而且默认的就是存储。此外,文档被解析,每个被标记成 stored 的字段可能都存储在索引中(也就是说,store 属性设置为 true;否则,默认情况下,字段不存储)。最后,字段值可以配置成 indexed。这意味着分析该字段值,划分为标记,并放置在索引中。
综上所述,一个字段可能以如下方式存储在索引中:
- 作为_source文档的一部分;
- 一个存储并未经解析的值;
- 一个解析成若干标记的值。
使用其他脚本语言
聚合
提供基于搜索查询的聚合功能,它基于简单的结构建立而成,可用进行组合以便于构建复杂的数据,所以称为聚合。聚合可以被看作是在一组文档上(查询得到)分析得到的信息的结果。
Metric 度量聚合
min、max、sum、avg 聚合
min、max、sum 和 avg 聚合的使用很相似。它们对于给定字段分别返回最小值、最大值、总和和平均值。任何数值型字段都可以作为这些值的源。比如下面通过 grade
计算平均值然后返回字段标识为 avg_grade
POST /exams/_search?size=0
{
"aggs" : {
"avg_grade" : { "avg" : { "field" : "grade" } }
}
}
# response
{
...
"aggregations": {
"avg_grade": {
"value": 75.0
}
}
}
value count
value_count 聚合跟前面描述的聚合类似,只是输入字段不一定要是数值型的。
POST /sales/_search?size=0
{
"aggs" : {
"types_count" : { "value_count" : { "field" : "type" } }
}
}
# response
{
...
"aggregations": {
"types_count": {
"value": 7
}
}
}
脚本聚合
使用 script 做到 value_count 聚合
POST /sales/_search?size=0
{
"aggs" : {
"type_count" : {
"value_count" : {
"script" : {
"source" : "doc['type'].value"
}
}
}
}
}
stats 和 extended_status(更多信息) 聚合
stats 和 extended_stats 聚合可以看成是在单一聚合对象中返回所有前面描述聚合的一种聚合。
POST /exams/_search?size=0
{
"aggs" : {
"grades_stats" : { "stats" : { "field" : "grade" } }
}
}
# response
{
...
"aggregations": {
"grades_stats": {
"count": 2,
"min": 50.0,
"max": 100.0,
"avg": 75.0,
"sum": 150.0
}
}
}
GET /exams/_search
{
"size": 0,
"aggs" : {
"grades_stats" : { "extended_stats" : { "field" : "grade" } }
}
}
# response
{
...
"aggregations": {
"grades_stats": {
"count": 2,
"min": 50.0,
"max": 100.0,
"avg": 75.0,
"sum": 150.0,
"sum_of_squares": 12500.0,
"variance": 625.0,
"std_deviation": 25.0,
"std_deviation_bounds": {
"upper": 125.0,
"lower": 25.0
}
}
}
}
更多查看文档
Bucketing 桶聚合
桶聚合返回很多子集,并限定输入数据到一个特殊的叫做桶的子集中。
terms 聚合
terms 聚合为字段中每个词条返回一个桶。这允许你对生成字段每个值得统计。
GET /_search
{
"aggs" : {
"genres" : {
"terms" : { "field" : "genre" }
}
}
}
# response
{
...
"aggregations" : {
"genres" : {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets" : [
{
"key" : "electronic",
"doc_count" : 6
},
{
"key" : "rock",
"doc_count" : 3
},
{
"key" : "jazz",
"doc_count" : 2
}
]
}
}
}
range 聚合
range 聚合使用定义的范围来创建桶。
GET /_search
{
"aggs" : {
"price_ranges" : {
"range" : {
"field" : "price",
"ranges" : [
{ "to" : 100.0 },
{ "from" : 100.0, "to" : 200.0 },
{ "from" : 200.0 }
]
}
}
}
}
# response
{
...
"aggregations": {
"price_ranges" : {
"buckets": [
{
"key": "*-100.0",
"to": 100.0,
"doc_count": 2
},
{
"key": "100.0-200.0",
"from": 100.0,
"to": 200.0,
"doc_count": 2
},
{
"key": "200.0-*",
"from": 200.0,
"doc_count": 3
}
]
}
}
}
date_range 聚合
date_range 聚合类似于 range,但它专用在使用日期类型的字段。
POST /sales/_search?size=0
{
"aggs": {
"range": {
"date_range": {
"field": "date",
"format": "MM-yyy",
"ranges": [
{ "to": "now-10M/M" },
{ "from": "now-10M/M" }
]
}
}
}
}
# response
{
...
"aggregations": {
"range": {
"buckets": [
{
"to": 1.4436576E12,
"to_as_string": "10-2015",
"doc_count": 7,
"key": "*-10-2015"
},
{
"from": 1.4436576E12,
"from_as_string": "10-2015",
"doc_count": 0,
"key": "10-2015-*"
}
]
}
}
}
missing 聚合
基于字段数据的单个桶集合,创建当前文档集上下文中缺少字段值(实际上缺少字段或设置了配置的 NULL 值)的所有文档的桶。 此聚合器通常会与其他字段数据存储桶聚合器(如 range)一起使用,以返回由于缺少字段数据值而无法放置在其他存储桶中的所有文档的信息。
POST /sales/_search?size=0
{
"aggs" : {
"products_without_a_price" : {
"missing" : { "field" : "price" }
}
}
}
# response
{
...
"aggregations" : {
"products_without_a_price" : {
"doc_count" : 00
}
}
}
nested 聚合
针对 nested 结构的聚合
GET /_search
{
"query" : {
"match" : { "name" : "led tv" }
},
"aggs" : {
"resellers" : {
"nested" : {
"path" : "resellers" # nested
},
"aggs" : {
"min_price" : { "min" : { "field" : "resellers.price" } }
}
}
}
}
# response
{
...
"aggregations": {
"resellers": {
"doc_count": 0,
"min_price": {
"value": 350
}
}
}
}
histogram 聚合
想象为柱状图一样,针对某个 field 的多个值进行聚合。
POST /sales/_search?size=0
{
"aggs" : {
"prices" : {
"histogram" : {
"field" : "price",
"interval" : 50
}
}
}
}
# response
{
...
"aggregations": {
"prices" : {
"buckets": [
{
"key": 0.0,
"doc_count": 1
},
{
"key": 50.0,
"doc_count": 1
},
{
"key": 100.0,
"doc_count": 0
},
{
"key": 150.0,
"doc_count": 2
},
{
"key": 200.0,
"doc_count": 3
}
]
}
}
}
跟多其他的聚合可以查看官方文档.
建议器
我们可以把建议器看成这样的功能:在考虑性能的情况下,允许纠正用户的拼写错误,以及构建一个自动完成功能。
建议器类型
- term:更正每个传入的单词,在非短语查询中很有用。
- phrase:工作在短语上,返回一个恰当的短语。
- completion:在索引中存储复杂的数据结构,提供快速高效的自动完成功能。
- context:配合 completion 使用,给其搜索文档字段提供某些上下文信息以供更好的进行 completion
使用
如果我们需要在查询结果中得到建议,例如使用 match
查询并尝试为 tring out Elasticsearc 短语得到一个建议,该短语包含一个拼写错误的词条。为此我们可以:
POST twitter/_search
{
"query" : {
"match": {
"message": "tring out Elasticsearch"
}
},
"suggest" : {
"my-suggestion" : {
"text" : "trying out Elasticsearch",
"term" : {
"field" : "message"
}
}
}
}
如果希望为同样的文本得到多个建议,则可把建议嵌入 suggest 对象中,并把 text 属性设置为选择的建议对象。(省略 query 结构)
POST _search
{
"suggest": {
"my-suggest-1" : {
"text" : "tring out Elasticsearch",
"term" : {
"field" : "message"
}
},
"my-suggest-2" : {
"text" : "kmichy",
"term" : {
"field" : "user"
}
}
}
}
# response
{
"took" : 105,
"timed_out" : false,
"_shards" : {
...
},
"hits" : {
[]
},
"suggest" : {
"my-suggest-1" : [
{
"text" : "tring", // 原始单测
"offset" : 0, // 原本的偏移值
"length" : 5, // 单词长度
"options" : [ // 建议
{
"text" : "trying", // 建议的文本
"score" : 0.8, // 建议的得分,越高越好
"freq" : 2 // 建议的频率,代表我们执行建议查询的索引上,该单词出现在文档中的次数
}
]
},
{
"text" : "out",
"offset" : 6,
"length" : 3,
"options" : [ ]
},
{
"text" : "elasticsearch",
"offset" : 10,
"length" : 13,
"options" : [ ]
}
],
"my-suggest-2" : [
{
"text" : "kmichy",
"offset" : 0,
"length" : 6,
"options" : [
{
"text" : "kimchy",
"score" : 0.8333333,
"freq" : 2
}
]
}
]
}
}
这里 suggest 的命名只是示例,最好使用有意义的命名。
建议器公用配置
- text:这个选项定义了我们希望得到建议的文本。此参数是必须的。
- field:这是另一个必须提供的参数。field参数设置了为哪个字段生成建议。
- analyzer:这个选项定义了分析器的名字,该分析器用作分析text参数提供的文本。如果未设置,Elasticsearch将使用field参数所指定的字段所用的分析器。
- size:这个参数默认为5,指定了text参数中每个词条可以返回的建议的最大数字。
- sort:此选项允许指定 Elasticsearch 返回的建议如何排序。默认情况下,此选项设置成 score,Elasticsearch将首先按照建议的得分排,然后按文档频率,最后按词条排。第二个可能值为 frequency,意味着结果首先按文档频率排,然后按分数,最后按词条。
具体到各个建议器的使用推荐查看文档,玩法太多了。
Spring-源码解析-容器的功能扩展-BeanFactory功能扩展
之前分析是建立在BeanFactory
接口以及它的实现类 XmlBeanFactory
来进行分析的, ApplicationContext
包含了 BeanFactory
的所有功能,多数情况我们都会使用 ApplicationConetxt
。其加载方式如下:
ApplicationContext ctx = new ClassPathXmlApplicationContext("beanFactory.xml");
所以我们就以 ClassPathXmlApplicationContext
来作为分析的切入点:
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException {
this(configLocations, refresh, null);
}
public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {
super(parent);
setConfigLocations(configLocations);
if (refresh) {
refresh();
}
}
ClassPathXmlApplicationContext
可以对数组进行解析并进行加载。而对于解析及功能实现都在 refresh()
中实现。
设置配置路径
在 ClassPathXmlApplicationContext
中支持多个配置文件以数组方式同时传入:
public void setConfigLocations(String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}
此函数主要用于解析给定的路径数组,当然,如果数组中包含特殊符号,如${var}
,那么在resolvePath中会搜寻匹配的系统变量并替换。
扩展功能
置了路径之后,便可以根据路径做配置文件的解析以及各种功能的实现了。可以说 refresh
函数中包含了几乎 ApplicationContext 中提供的全部功能,而且此函数中逻辑非常清晰明了,使我们很容易分析对应的层次及逻辑。
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
// 准备刷新的上下文环境
prepareRefresh(); // 1
// Tell the subclass to refresh the internal bean factory.
// 初始化 BeanFactory,并进行 XML 文件读取
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); // 2
// Prepare the bean factory for use in this context.
// 对 BeanFactory 进行各种功能填充
prepareBeanFactory(beanFactory); // 3
try {
// Allows post-processing of the bean factory in context subclasses.
// 子类覆盖方法做额外的处理
postProcessBeanFactory(beanFactory); // 4
// Invoke factory processors registered as beans in the context.
// 激活各种 BeanFactory 处理器
invokeBeanFactoryPostProcessors(beanFactory); // 5
// Register bean processors that intercept bean creation.
// 注册拦截 Bean 创建的 Bean 处理器,这里只是注册,真正的调用是在 getBean 时候
registerBeanPostProcessors(beanFactory); // 6
// Initialize message source for this context.
// 为上下文初始化 Message 源,即不同语言的消息体,国际化处理
initMessageSource(); // 7
// Initialize event multicaster for this context.
// 初始化应用消息广播器,并放入 "applicationEventMulticaster" bean 中
initApplicationEventMulticaster(); // 8
// Initialize other special beans in specific context subclasses.
// 留给子类来初始化其他的 bean
onRefresh(); // 9
// Check for listener beans and register them.
// 在所有注册的 bean 中查找 Listerner bean,注册到消息广播器中
registerListeners(); // 10
// Instantiate all remaining (non-lazy-init) singletons.
// 初始化剩下的单实例(非惰性的)
finishBeanFactoryInitialization(beanFactory); // 11
// Last step: publish corresponding event.
// 完成刷新过程,通知生命周期处理器 lifecycleProcessor 刷新过程,同时发出 ContextRefreshEvent 通知别人
finishRefresh(); // 12
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}
-
初始化前的准备工作,例如对系统属性或者环境变量进行准备及验证。
在某种情况下项目的使用需要读取某些系统变量,而这个变量的设置很可能会影响着系统的正确性,那么
ClassPathXmlApplicationContext
为我们提供的这个准备函数就显得非常必要,它可以在 Spring 启动的时候提前对必须的变量进行存在性验证。 -
初始化BeanFactory,并进行XML文件读取。
之前有提到
ClassPathXmlApplicationContext
包含着BeanFactory
所提供的一切特征,那么在这一步骤中将会复用BeanFactory
中的配置文件读取解析及其他功能,这一步之后,ClassPathXmlApplicationContext
实际上就已经包含了BeanFactory
所提供的功能,也就是可以进行 Bean 的提取等基础操作了。 -
对BeanFactory进行各种功能填充。
@qualifier与@Autowired应该是大家非常熟悉的注解,那么这两个注解正是在这一步骤中增加的支持。
-
子类覆盖方法做额外的处理。
Spring 之所以强大,除了它功能上为大家提供了便例外,还有一方面是它的完美架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。
-
激活各种BeanFactory处理器。
-
注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean时候。
-
为上下文初始化 Message 源,即对不同语言的消息体进行国际化处理。
-
初始化应用消息广播器,并放入
applicationEventMulticaster
bean 中。 -
留给子类来初始化其他的bean。
-
在所有注册的bean中查找 listener bean,注册到消息广播器中。
-
初始化剩下的单实例(非惰性的)。
-
完成刷新过程,通知生命周期处理器
lifecycleProcessor
刷新过程,同时发出ContextRefreshEvent
通知别人。
环境准备
prepareRefresh
函数主要是做些准备工作,例如对系统属性及环境变量的初始化及验证。
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}
// Initialize any placeholder property sources in the context environment
// 留给子类覆盖
initPropertySources();
// Validate that all properties marked as required are resolvable
// see ConfigurablePropertyResolver#setRequiredProperties
// 验证需要的属性文件是否都已经放入环境中
getEnvironment().validateRequiredProperties();
// Allow for the collection of early ApplicationEvents,
// to be published once the multicaster is available...
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}
initPropertySources
正符合 Spring 的开放式结构设计,给用户最大扩展 Spring 的能力。用户可以根据自身的需要重写initPropertySources
方法,并在方法中进行个性化的属性处理及设置。validateRequiredProperties
则是通过继承重写的方式对属性进行验证。
加载 BeanFactory
经过 obtainFreshBeanFactory
之后 ApplicationContext
就已经拥有了 BeanFactory
的全部功能。
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
// 初始化 BeanFactory,并进行 XML 文件读取,并将得到的 BeanFactory 记录在当前实体的属性中
refreshBeanFactory();
// 返回当前实体的 beanFactory 属性
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
protected final void refreshBeanFactory() throws BeansException {
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 创建 DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory(); // 1
// 为了序列化指定 id,如果需要的话,让这个 BeanFactory 从 id 反序列化到 BeanFactory 对象
beanFactory.setSerializationId(getId()); // 2
// 定制 beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖
customizeBeanFactory(beanFactory); // 3
// 初始化 DocumentReader,并进行 XML 文件读取及解析
loadBeanDefinitions(beanFactory); // 4
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory; // 5
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}
- 创建 DefaultListableBeanFactory
- 指定序列化 ID
- 定制 BeanFactory
- 加载 BeanDefinition
- 使用全局变量记录 BeanFactory 类实例
定制 BeanFactory
增加是否允许覆盖是否允许扩展的设置(通过子类覆盖)
protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) {
// 如果属性 allowBeanDefinitionOverriding 不为空,设置给 beanFactory 对象相应属性
// 此属性的含义:是否允许覆盖同名称的不同定义的对象
if (this.allowBeanDefinitionOverriding != null) {
beanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// 如果属性 allowCircularReferences 不为空,设置给 beanFactory 对象相应属性,
// 此属性的含义:是否允许 bean 之间存在循环依赖
if (this.allowCircularReferences != null) {
beanFactory.setAllowCircularReferences(this.allowCircularReferences);
}
}
加载 BeanDefinition
与之前解析 XmlBeanFactory
一样,初始化 DefaultListableBeanFactory
之后需要 XmlBeanDefinitionReader
来读取 XML,接下来是初始化 XmlBeanDefinitionReader
的步骤
protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// Create a new XmlBeanDefinitionReader for the given BeanFactory.
// 为指定的 BeanFactory 创建 XmlBeanDefinitionReader
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
// Configure the bean definition reader with this context's
// resource loading environment.
// 对 beanDefinitionReader 进行环境变量的设置
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
// Allow a subclass to provide custom initialization of the reader,
// then proceed with actually loading the bean definitions.
// 对 BeanDefinitionReader 进行设置,可以覆盖
initBeanDefinitionReader(beanDefinitionReader);
loadBeanDefinitions(beanDefinitionReader);
}
在初始化了 DefaultListableBeanFactory
和 XmlBeanDefinitionReader
后就可以进行配置文件的读取了
protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}
接下来的套路之前基本都已经说到了。
功能扩展
在进入函数 prepareBeanFactory
前,Spring 已经完成了对配置的解析,而 ApplicationContext
在功能上的扩展也由此展开。
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// Tell the internal bean factory to use the context's class loader etc.
// 设置 beanFactory 的 classLoader 为当前 context 的 classLoader
beanFactory.setBeanClassLoader(getClassLoader());
// 设置 beanFactory 的表达式语言处理器,默认可使用 #{bean.xxx} 的形式来调用相关属性值
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
// 为beanFactory 增加了一个默认的 propertyEditor,这个主要是对 bean 的属性等设置管理的一个工具
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
// Configure the bean factory with context callbacks.
// 添加 BeanPostProcessor
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 设置了几个忽略自动装配的接口
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
// BeanFactory interface not registered as resolvable type in a plain factory.
// MessageSource registered (and found for autowiring) as a bean.
// 设置了几个自动装配的特殊规则
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);
// Register early post-processor for detecting inner beans as ApplicationListeners.
// 4.3.4 后添加的
// 避免 BeanPostProcessor 被 getBeanNamesForType 调用
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));
// Detect a LoadTimeWeaver and prepare for weaving, if found.
// 增加对 AspectJ 的支持
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
// Register default environment beans.
// 添加默认的系统环境 bean
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}
上面函数主要进行了几个方面的扩展
- 增加对
SPEL
语言的支持。 - 增加对属性编辑器的支持。
- 增加对一些内置类,比如
EnvironmentAware
、MessageSourceAware
的信息注入。 - 设置了依赖功能可忽略的接口。
- 注册一些固定依赖的属性。
- 注册
ApplicationListenerDetector
- 增加AspectJ的支持(会在后面进行详细的讲解)。
- 将相关环境变量及属性注册以单例模式注册。
增加 SPEL 语言的支持
在运行时构建复杂表达式、存取对象图属性、对象方法调用等,并且能与 Spring 功能完美整合,比如能用来配置bean 定义。SpEL 是单独模块,只依赖于 core 模块,不依赖于其他模块,可以单独使用。
我们进入这个方法发现它只是简单指定了一个 StandardBeanExpressionResolver
进行语言解析器的注册,之后在我们之前说的 bean
进行初始化时候的属性填充时候会调用 AbstractAutowireCapableBeanFactory
类的 applyPropertyValues
函数来完成功能。同时,也是在这个步骤中一般通过 AbstractBeanFactory
中的 evaluateBeanDefinitionString
方法区完成 SPEL 的解析。
protected Object evaluateBeanDefinitionString(String value, BeanDefinition beanDefinition) {
if (this.beanExpressionResolver == null) {
return value;
}
Scope scope = (beanDefinition != null ? getRegisteredScope(beanDefinition.getScope()) : null);
return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope));
}
增加属性注册编辑器
对于自定义属性的注入如果在原生属性编辑器(PropertyEditor
)不支持的情况下,可以使用两种方法:
-
自定义属性编辑器
继承
PropertyEditorSupport
重写setAsText
方法 -
注册 Spring 自带的属性编辑器
CustomDateEditor
在这里 beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));
这段语句中我们可以探究一下 ResourceEditorRegistrar
的实现其中的 doRegisterEditor
方法中(被核心方法 registerCustomEditors
调用)可以看到提到了自定义属性中使用的关键代码。
private void doRegisterEditor(PropertyEditorRegistry registry, Class<?> requiredType, PropertyEditor editor) {
if (registry instanceof PropertyEditorRegistrySupport) {
((PropertyEditorRegistrySupport) registry).overrideDefaultEditor(requiredType, editor);
}
else {
registry.registerCustomEditor(requiredType, editor);
}
}
此时我们再回头看 registerCustomEditors
可以看到内部无非是注册了一系列的常用类型的属性编辑器。那什么时候能调用到它呢?
借助 idea 的调用链工具我们可以看到
在 AbstractBeanFactory
中的 registerCustomEditors
方法中被调用过,继续下去我们可以看到一个熟悉的方法就是 AbstractBeanFactory
类中的 initBeanWrapper
方法,这是在 bean 初始化时候使用的一个方法,详见。
此时,逻辑已经明了了,在 bean 的初始化之后会调用 ResourceEditorRegistrar
的 registerCustomEditors
方法进行批量的通用属性编辑器注册。注册后,在属性填充的环节便可以直接让 Spring 使用这些编辑器进行熟悉的解析了。
既然提到了BeanWrapper
,这里也有必要强调下,Spring 中用于封装 bean 的是 BeanWrapper
类型,而它又间接继承了 PropertyEditorRegistry
类型,也就是我们之前反复看到的方法参数 PropertyEditorRegistry registry
,其实大部分情况下都是 BeanWrapper
,对于 BeanWrapper
在 Spring 中的默认实现是BeanWrapperImpl
,而 BeanWrapperImpl
除了实现 BeanWrapper
接口外还继承了PropertyEditorRegistrySupport
,在 PropertyEditorRegistrySupport
中有这样一个方法 createDefaultEditors
定义了一系列常用的属性编辑器方便我们进行配置。
添加 ApplicationContextAwareProcessor
处理器
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
(不加这行自己都忘了自己说到了哪儿)
我们继续通过 AbstractApplicationContext
的 prepareBeanFactory
方法的主线来进行函数跟踪。对于 beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this))
其实主要目的就是注册个 BeanPostProcessor
,而真正的逻辑还是在 ApplicationContextAwareProcessor
中。
ApplicationContextAwareProcessor
实现 BeanPostProcessor
接口,我们回顾下之前讲过的内容,在 bean 实例化的时候,也就是 Spring 激活 bean 的 init-method
的前后,会调用 BeanPostProcessor
的 postProcessBeforeInitialization
方法和 postProcessAfterInitialization
方法。同样,对于ApplicationContextAwareProcessor
我们也关心这两个方法。
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
return bean;
}
… 过
@Override
public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
AccessControlContext acc = null;
if (System.getSecurityManager() != null &&
(bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware || bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware || bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
acc = this.applicationContext.getBeanFactory().getAccessControlContext();
}
if (acc != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
invokeAwareInterfaces(bean);
return null;
}
}, acc);
}
else {
invokeAwareInterfaces(bean);
}
return bean;
}
private void invokeAwareInterfaces(Object bean) {
if (bean instanceof Aware) {
if (bean instanceof EnvironmentAware) {
((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof EmbeddedValueResolverAware) {
((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
}
if (bean instanceof ResourceLoaderAware) {
((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {
((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
}
if (bean instanceof MessageSourceAware) {
((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationContextAware) {
((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}
}
}
实现这些 Aware
接口的 bean 在被初始化之后,可以取得一些对应的资源。
设置忽略依赖
invokeAwareInterfaces
方法中间接调用的 Aware
类已经不是普通的 bean 了,如 ResourceLoaderAware
、ApplicationEventPublisherAware
等,那么当然需要在 Spring 做 bean 的依赖注入的时候忽略它们。而 ignoreDependencyInterface
的作用正是在此。
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);
注册依赖
Spring中有了忽略依赖的功能,当然也必不可少地会有注册依赖的功能。
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);
当注册了依赖解析后,例如当注册了对 BeanFactory.class
的解析依赖后,当 bean 的属性注入的时候,一旦检测到属性为 BeanFactory
类型便会将 beanFactory
的实例注入进去。
浅谈动态规划
本篇是个人动态规划笔记。
应用动态规划方法:
- 刻画一个最优解的结构特征
- 递归地定义最优解的值
- 计算最优解的值,通常采用自底向上的方法
- 利用计算出的信息构造一个最优解
动态规划两种等价的实现方法:
- 带备忘的自顶向下法:此方法仍按自然的递归形式编写过程,但过程会保存每个子问题的解(通常保存在一个数组或散列表中)。当需要一个子问题的解时,过程首先检查是否保存过此解。如果是,则直接返回保存的值,从而节省了计算时间,否则,按通常方式计算这个子问题。
- 自底向上法:这种方法一般需要恰当定义子问题『规模』的概念,使得任何子问题的求解都只依赖于『更小的』子问题的求解。因而我们可以将子问题按规模排序,按由小至大的顺序进行求解。当求解某个子问题时,它所引来的那些更小的子问题都已求解完毕,结果已经保存。每个子问题只需求解一次,当我们求解它(也是第一次遇到它)时,它的所有前提子问题都已求解完成。
适合动态规划求解的两个要素
最优子结构
如果一个问题的最优解包含其子问题的最优解没我们就称此问题具有最优子结构性质。
发掘此问题具有最优子结构性质的通用模式:
- 证明问题最优解的第一个组成部分是做出一个选择,做出这次选择产生一个或多个待解的子问题。
- 对于一个给定问题,在其可能的第一步选择中,你假定已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择。
- 给定可获得最优解的选择后,你确定这次选择会产生哪些子问题,已经如何最好地刻画子问题空间。
- 利用『剪切-粘贴』(cut-and-paste) 技术证明:作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。证明这一点是利用反证法:假定子问题的解不是其自身的最优解,那么我们就可以从原问题的解中『剪切』掉这些非最优解,将最优解『粘贴』进去,从而得到原问题一个最优的解,这与最初的解释原问题最优解的前提假设矛盾。
一个刻画子问题空间的好经验是:保持子问题空间尽可能简单,只在必要时才扩展它。
对于不同领域,最优子结构的不同体现在两个方面:
- 原问题的最优解中设计多少个子问题,以及
- 在确定最优解使用哪些子问题时,我们需要考察多少种选择
算法导论有两个实例值得一看。
重叠子问题
适合用动态规划方法求解的最优化问题应该具备的第二个性质是子问题空间必须足够『小』,即问题的递归算法会反复地求解相同的子问题,而不是一直声称新的子问题,即具有重复子问题性质。
实例
参考书籍:
《算法导论》
select, poll, epoll, 信号驱动 IO细讲
63章 其他备选的 IO 模型
整体概览
实际上 I/O 多路复用,信号驱动 I/O 以及 epool 都是用来实现同一个目标的技术——同时检查多个文件描述符,看它们是否准备好了执行 I/O 操作(准确地说,是看 I/O 系统调用是否可以非阻塞地执行)。文件描述符就绪状态的转化是通过一些 I/O 事件来触发的,比如输入数据到达,套接字连接建立完成,或者是之前满载的套接字发送缓冲区在 TCP 将队列中的数据传送到对端之后由了剩余空间。同时检查多个文件描述符在类似网络服务器的应用中很有用处,或者是那么必须同事检查终端以及管道或套接字输入的应用程序。
需要注意的是这些技术都不会执行实际的 I/O 操作。它们只是告诉我们某个文件描述符已经处于就绪状态了,这时需要调用其他的系统调用来完成实际的 I/O 操作。
水平触发和边缘触发
- 水平触发通知:如果文件描述符上可以非阻塞地执行 I/O 系统调用,此时任务它已经就绪。
- 边缘触发通知:如果文件描述符自上次状态检查以来有了新的 I/O 活动(比如新的输入),此时需要触发通知。
I/O 模式 | 水平触发 | 边缘触发 |
---|---|---|
select(), pool() | √ | |
信号驱动 I/O | √ | |
epool | √ | √ |
当采用水平触发通知时,我们可以在任意时刻检查文件描述符的就绪状态。这表示当我们确定了文件描述符处于就绪态时(比如存在输入数据),就可以对其执行一些 I/O 操作,然后重复检查文件描述符,看看是否仍然处于就绪态(比如还有更多的输入数据),此时我们就能执行更多的 I/O,以此类推。换句话说,由于水平触发模式允许我们在任意时刻重复检查 I/O 状态,没有必要每次当文件描述符就绪后需要尽可能多地执行 I/O (也就是尽可能多地读取字节,亦或是根本不去执行任何 I/O)。
与此相反的是,当我们采用边缘触发时,只有当 I/O 事件发生时我们才会收到通知。在另一个 I/O 事件到来前我们不会收到任何新的通知。另外,当文件描述符收到 I/O 事件通知时,通常我们并不知道要处理多少 I/O(例如有多少字节可读)。因此,采用边缘触发通知的程序通常要按照如下规则来设计。
- 在接收到一个 I/O 事件通知后,程序在某个时刻应该在相应的文件描述符上尽可能多地执行 I/O(比如尽可能多地读取字节)。如果程序没有这么做,那么就可能失去执行 I/O 的机会。因为直到产生另一个 I/O 事件位置,在此之前程序都不会再接收到通知了,因此也就不知道此时应该执行 I/O 操作。这将导致数据丢失或者程序中出现阻塞。
- 如果程序采用循环来对文件描述符执行尽可能多的 I/O ,而文件描述符又被置为可阻塞的,那么最终当没有更多的 I/O 可执行时,I/O 系统调用就会被阻塞。基于这个原因,每个被检查的文件描述符通常都应该置为非阻塞模式,在得到 I/O 事件通知后重复执行 I/O 操作,直到相应的系统调用(比如 write(), read()) 以错误码 EAGAIN 或 EWOULDBLOC�K 的形式失联。
I/O 多路复用
select() 系统调用
系统调用 select()
会一直阻塞,直到一个或多个文件描述符集合成为就绪态。
#include<sys/time.h>
#include<sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// Retrun number of ready file descriptors, 0 on timeout, or -1 on error.
参数 readfds
, writefds
以及 exceptfds
都是指向文件描述符集合的指针,所指向的数据类型是 fd_set,。这些参数按照如下方式使用。
-
readfds
是用来检测输入是否就绪的文件描述符集合 -
writefds
是用来检测输出是否就绪的文件描述符集合 -
exceotfds
是用来检测异常情况是否发生的文件描述符集合在 Linux 上,一个异常情况只有下面两种情况下发生:
- 连接到处于信号模式下的伪终端设备上的从设备状态发生了改变
- 流式套接字上接收到了带外数据
通常,数据类型 fd_set
以位掩码的形式来实现。但是是由下面四个宏来完成
#include <sys/select.h>
void FD_ZERO(fd_set *fdset); // 初始化为空
void FD_SET(int fd, fd_set *fdset); // 将文件描述符 fd 添加入集合
void FD_CLR(int fd, fd_set *fdset); // 将 fd 移除
int FD_ISSET(int fd, fd_set *fdset); // fd 是否在集合中,是返回1,否则返回0
文件描述符集合有一个最大容量限制,由常量 FD_SETSIZE 来决定。在 Linux 上,该常量的值为1024。
参数readfds
、writefds
和 exceptfds
所指向的结构体都是保存结果值的地方。在调用 select()
之前,这些参数指向的结构体必须初始化(通过FD_ZERO() 和 FD_SET()
),以包含我们感兴趣的文件描述符集合。之后 select()
调用会修改这些结构体,当 select()
返回时,它们包含的就是已处于就绪态的文件描述符集合了(值-结果参数)。(由于这些结构体会在调用中被修改,如果要在循环中反复调用select()
,我们必须保证每次都要重新初始化它们。)之后这些结构体可以通过 FD_ISSET()
来检查。
timeout 参数
参数 timeout
控制着 select()
的阻塞行为。该参数可指定为 NULL
,此时 select()
会一直阻塞。又或者是指向一个 timeval
结构体。
struct timeval {
time_t tv_sec; /*Seconds */
suseconds_t tv_usec; /* Microseconds (long int) */
};
如果结构体 timeval
的两个域都为0的话,此时 select()
不会阻塞,它只是简单地轮询指定的文件描述符集合,看看其中是否有就绪的文件描述符并立即返回。否则,timeout
将为 select()
指定一个等待时间的上限值。
当 timeout
设为 NULL
,或其指向的结构体字段非零时, select()
将阻塞直到有下列事件发生:
readfds
、writefds
或exceptfds
中指定的文件描述符中至少有一个成为就绪态;- 该调用被信号处理例程中断
timeout
中指定的时间上限已超时
select()
返回所在3个集合中被标记为就绪态的文件描述符总数。如果返回 -1 则是错误发生,包括 EBADF
和 EINTR
。如果返回 0 则说明超时。
示例程序
#include <sys/time.h>
#include <sys/select.h>
#include "t_select.h"
#include "tlpi_hdr.h"
static void
usageError(const char *progName)
{
fprintf(stderr, "Usage: %s {timeout|-} fd-num[rw]...\n", progName);
fprintf(stderr, " - means infinite timeout; \n");
fprintf(stderr, " r = monitor for read\n");
fprintf(stderr, " w = monitor for write\n\n");
fprintf(stderr, " e.g.: %s - 0rw 1w\n", progName);
exit(EXIT_FAILURE);
}
/**
* 介绍一下用法
* ./basic 10 0r 1w 第一个10是超时秒数, 0是文件描述符号,r代表读文件,w 代表写文件
* 文件描述符中,0是标准输入,1是标准输出
*/
void
testSelect(int argc, char *argv[])
{
fd_set readfds, writefds;
int ready, nfds, fd, numRead, j;
struct timeval timeout;
struct timeval *pto;
char buf[10]; /* Large enough to hold "rw\0" */
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageError(argv[0]);
/* Timeout for select() is specified in argv[1] */
if (strcmp(argv[1], "-") == 0) {
pto = NULL; /* Infinite timeout */
} else {
pto = &timeout;
// 输入中获取时间
timeout.tv_sec = getLong(argv[1], 0, "timeout");
timeout.tv_usec = 0; /* No microseconds */
}
/* Process remaining arguments to build file descriptor sets */
// 初始化读写描述符集合
nfds = 0;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
// 分别将读描述符和写描述符加入
for (j = 2; j < argc; j++) {
numRead = sscanf(argv[j], "%d%2[rw]", &fd, buf);
if (numRead != 2)
usageError(argv[0]);
if (fd >= FD_SETSIZE)
cmdLineErr("file descriptor exceeds limit (%d)\n", FD_SETSIZE);
if (fd >= nfds)
nfds = fd + 1; /* Record maximum fd + 1 */
if (strchr(buf, 'r') != NULL)
FD_SET(fd, &readfds);
if (strchr(buf, 'w') != NULL)
FD_SET(fd, &writefds);
}
/* We've built all of the arguments; now call select() */
// 调用
ready = select(nfds, &readfds, &writefds, NULL, pto);
/* Ignore exceptional events */
if (ready == -1)
errExit("select");
/* Display results of select() */
printf("ready = %d\n", ready);
for (fd = 0; fd < nfds; fd++)
printf("%d: %s%s\n", fd, FD_ISSET(fd, &readfds) ? "r" : "",
FD_ISSET(fd, &writefds) ? "w" : "");
if (pto != NULL)
printf("timeout after select(): %ld.%03ld\n",
(long) timeout.tv_sec, (long) timeout.tv_usec / 1000);
exit(EXIT_SUCCESS);
}
poll()
系统调用
系统调用 poll()
执行的任务同 select()
很相似。两者间主要的区别在于我们要如何制定待检查的文件描述符。在 select()
中,我们提供三个集合,在每个集合中标明我们感兴趣的文件描述符。而在 poll()
中我们提供一列文件描述符,并在每个文件描述符上标明我们感兴趣的事件。
#include <poll.h>
int poll(struct pollfd fds[], nfds_t nfds, int timeout);
// Returns number of ready file descriptors, 0 on timeout, or -1 on error
参数 fds
列出了我们需要 poll()
来检查的文件描述符。该参数为 pollfd
结构体数组,其定义如下。
struct pollfd {
int fd; /* File descriptor */
short events; /* Requested events bit mask */
short revents; /* Returned events bit mask */
};
pollfd
结构体中的 events
和 revents
字段都是位掩码。调用者初始化 events
来指定需要为描述符 fd
做检查的事件。当 poll()
返回时,revents
被设定以此来表示该文件描述符上实际发生的事件。
输入事件相关位掩码
位掩码 | events 中的输入 | 返回到 revents | 描述 |
---|---|---|---|
POLLIN | �√ | √ | 可读取非高优先级的数据 |
POLLRDNORM | √ | √ | 等同于 POLLIN |
POLLRDBAND | √ | √ | 可读取优先级数据(Linux 中不使用) |
POLLPRI | √ | √ | 可读取高优先级数据 |
POLLRDHUP | √ | √ | 对端套接字关闭 |
输出事件相关位掩码
位掩码 | events 中的输入 | 返回到 revents | 描述 |
---|---|---|---|
POLLOUT | √ | √ | 普通数据可写 |
POLLWRNORM | √ | √ | 等同于 POLLOUT |
POLLWRBAND | √ | √ | 优先级数据可写入 |
返回有关文件描述符附加信息的位掩码
位掩码 | events 中的输入 | 返回到 revents | 描述 |
---|---|---|---|
POLLERR | √ | 有错误发生 | |
POLLHUP | √ | 出现挂断 | |
POLLNVAL | √ | 文件描述符未打开 |
timeout 参数
- -1:阻塞直到有一个文件描述符达到就绪态或者捕获到一个信号
- 0: 不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态
- 大于0:至多阻塞 timeout 毫秒,知道 fds 列出的文件描述符中有一个达到就绪态,或者知道捕获到一个信号位置。
返回值
- -1:有错误
- 0:超时
- 大于0:表示数组 fds 重有用非零 revents 字段的 pollfd 结构体数量
示例程序
#include <time.h>
#include <poll.h>
#include "poll_pipes.h"
#include "tlpi_hdr.h"
/**
* 这个程序创建了一些管道(每个管道使用一对连续的文件描述符),将
* 字节写到随机选择的管道写端,然后通过 poll() 来检查看哪个管道中有数据可进行读取
*/
int
testPoll(int argc, char *argv[])
{
int numPipes, j, ready, randPipe, numWrites;
int (*pfds)[2]; /* File descriptors for all pipes */
struct pollfd *pollFd;
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s num-pipes [num-writes]\n", argv[0]);
// 管道数
numPipes = getInt(argv[1], GN_GT_0, "num-pipes");
// 分配读写管道文件描述符内存
pfds = calloc(numPipes, sizeof(int [2]));
if (pfds == NULL)
errExit("malloc");
// 对于每对管道分配一个 pollFd 内存
pollFd = calloc(numPipes, sizeof(struct pollfd));
if (pollFd == NULL)
errExit("malloc");
for (j = 0; j < numPipes; j++)
// 创建管道, pfds[j][0] 是读管道,pfds[j][1] 是写管道
if (pipe(pfds[j]) == -1)
errExit("pipe %d", j);
// 可写数据管道数
numWrites = (argc > 2) ? getInt(argv[2], GN_GT_0, "num-writes") : 1;
srandom((int) time(NULL));
for (int j = 0; j < numWrites; ++j) {
randPipe = random() % numPipes;
printf("Writing to fd: %3d (read fd: %3d)\n",
pfds[randPipe][1], pfds[randPipe][0]);
// 写数据进入管段
if (write(pfds[randPipe][1], "a", 1) == -1)
errExit("write %d", pfds[randPipe][1]);
}
// 设置 pollFd
for (int j = 0; j < numPipes; ++j) {
pollFd[j].fd = pfds[j][0];
pollFd[j].events = POLLIN;
}
// 调用 poll
ready = poll(pollFd, numPipes, -1);
if (ready == -1)
errExit("poll");
printf("poll() returned: %d\n", ready);
// 得到哪些管段被写然后可读
for (int j = 0; j < numPipes; ++j) {
if (pollFd[j].revents & POLLIN)
printf("Readable: %d %3d\n", j, pollFd[j].fd);
}
}
文件描述符何时就绪
select()
和 poll()
只会告诉我们 I/O 操作是否会阻塞,而不是告诉我们到底能否成功传输数据。
普通文件
代表普通文件的文件描述符总是被 select()
标记为可读和可写。对于 poll()
来说,则会在 revents
字段返回 POLLIN 和 POLLOUT 标志。原因如下:
read()
总是会立刻返回数据、文件结尾符或者错误write()
总是会立刻传输数据或者因出现某些错误而失败
终端和伪终端
在终端和伪终端上 select()
和 poll()
所代表的含义
条件或事件 | select() |
poll() |
---|---|---|
有输入 | r | POLLIN |
可输出 | w | POLLOUT |
伪终端对端调用 close() 后 |
rw | POLLHUP |
处于信包模式下的伪终端主设备检测到从设备端状态改变 | x | POLLPRI |
管道和 FIFO
select()
和 poll()
在管道或 FIFO 读端上的通知
条件或事件 | select() |
poll() |
---|---|---|
管道无数据,�写端没打开 | r | POLLHUP |
管道有数据,写端打开 | r | POLLIN |
管道有数据,写端没打开 | r | POLLIN|POLLHUP |
select()
和 poll()
在管道或 FIFO 写端上的通知
条件或事件 | select() |
poll() |
---|---|---|
没有 PIPE_BUF 个字节空间,�读端没打开 | w | POLLERR |
有 PIPE_BUF 个字节空间,�读端打开 | w | POLLOUT |
有 PIPE_BUF 个字节空间,�读端没打开 | w | POLLOUT|POLLERR |
套接字
条件或事件 | select() |
poll() |
---|---|---|
有输入 | r | POLLIN |
可输出 | w | POLLOUT |
在监听套接字上建立连接 | r | POLLIN |
接收到带外数据(只限 TCP) | x | POLL |
流套接字的对端关闭连接或执行了 shutdown(SHUT_WR) |
rw | POLLIN|POLLOUT|POLLRDHUP |
比较 select()
和 poll()
实现细节
在 Linux 内核层面,select()
和 poll()
都使用了相同的内核 poll
例程集合。这些例程有别于系统调用 poll()
本身。每个例程都返回有关单个文件描述符就绪的信息。这个就绪信息以位掩码的形式返回,其值同 poll()
系统调用中返回的 revents
字段中的比特值相关。 poll()
系统调用的实现包括为每个文件描述符调用内核 poll
例程,并将结果信息填到对应的 revents
字段中去。
API 之间的区别
select()
有文件描述符上限- 由于
select()
的参数fd_set
同事也是保存调用结果的地方,如果要在循环中重复调用select()
的话,我们必须每次都要重新初始化fd_set
。而poll()
通过独立的两个字段events
(针对输入)和revents
(针对输出)来处理,从而避免每次都要重新初始化参数。 select()
提供的超时精度比较高- 如果其中一个被检查的文件描述符关闭了,通过在对应的
revents
字段中设定POLLNVAL
标记,poll()
会准确高数我们是哪一个文件描述符关闭了。与之相反,select()
只会返回 -1,并设错误码为EBADF
。通过在描述符上执行 I/O 系统调用并检查错误码,让我们自己来判断哪个文件描述符关闭了。
性能
当满足如下两条中任意一条时,poll()
和 select()
将具有相似的性能表现。
- 待检查的文件描述符范围较小
- 有大量的文件描述符待检查,但是它们分布得很密集。
然而,如果被检查的文件描述符集合很稀疏的话,select()
和 poll()
的性能差异将变得非常明显,在这种情况下,后者更优。
select()
和 poll()
存在的问题
当检查大量的文件描述符时,这两个 API 都会遇到一些问题
- 每次调用
select()
或poll()
,内核都必须检查所有被指定的文件描述符,看它们是否处于就绪态。当检查大量处于密集范围,该挫折耗费时间将大大超过接下来的操作。 - 每次调用
select()
或poll()
,程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核,内核检查过描述符后,修改这个数据结构并返回给程序。(此外,对于select()
来说,我们还必须在每次调用前初始化这个数据结构。)对于poll()
来说,随着待检查的文件描述符数量的增加,传递给内核的数据结构大小也会随之增加。当检查大量文件描述符时,从用户控件到内核控件来回拷贝这个数据结构将占用大量的 CPU 时间。对于select()
来说,这个数据结构的大小固定为FD_SETSIZE
,与待检查的文件描述符数量无关。 select()
或poll()
调用完成后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符处于就绪态了。
信号驱动 IO
在信号驱动 I/O 中,当文件描述符上可执行 I/O 操作时,进程请求内核为自己发送一个信号。之后进程就可以执行任何其他的任务知道 I/O 就绪为止,此时内核会发送信号给进程。要使用信号驱动 I/O,程序需要按照如下步骤来执行。
-
为内核发送的通知信号安装一个信号处理例程。默认情况下,这个通知信号为
SIGIO
。 -
设定文件描述符的属主,也就是当文件描述符上可执行 I/O 时会接收到通知信号的进程或进程组。通常我们让调用进程成为属主。设定属主可通过
fcntl()
的F_SETOWN
操作来完成:fcntl(fd, F_SETOWN, pid);
-
通过设定
O_NONBLOCK
标志使能非阻塞 I/O。 -
通过打开
O_ASYNC
标志使能信号驱动 I/O。这可以和上一步合并为一个操作,因为它们都需要用到fcntl()
的F_SETFL
操作。flags = fcntl(fd, F_GETFL); fcntl(fd, F_SETFL, flags | O_ASYNC | ONONBLOCK);
-
调用进程现在可以执行其他的任务了。当 I/O 操作就绪时,内核为进程发送一个信号,然后调用在第 1 步中安装好的信号处理例程。
-
信号驱动 I/O 提供的是边缘触发通知。这表示一旦进程被通知 I/O 就绪,它就应该尽可能多地执行 I/O (例如尽可能多地读取字节)。假设文件描述符是非阻塞式的,这表示需要在循环中执行 I/O 系统调用直到失败为止,此时错误码
EAGAIN
或EWOULDBLOCK
。
示例程序
#include <signal.h>
#include <ctype.h>
#include <fcntl.h>
#include <termios.h>
#include "tlpi_hdr.h"
#include "tty_functions.h"
static volatile sig_atomic_t gotSigio = 0;
static void
sigioHandler(int sig)
{
gotSigio = 1;
}
int
testSigio(int argc, char *argv[])
{
int flags, j, cnt;
struct termios origTermios;
char ch;
struct sigaction sa;
Boolean done;
// 1
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigioHandler;
if (sigaction(SIGIO, &sa, NULL) == -1)
errExit("sigaction");
// 2
if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1)
errExit("fcntl(F_SETOWN)");
// 3 & 4
flags = fcntl(STDIN_FILENO, F_GETFL);
if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1)
errExit("fcntl(F_SETFL");
if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1)
errExit("ttySetCbreak");
for (done = FALSE, cnt = 0; !done; cnt++) {
for (j = 0; j < 10000000; j++)
continue;
// 被触发
if (gotSigio) {
while (read(STDIN_FILENO, &ch, 1) > 0 && !done) {
printf("cnt=%d; read %c\n", cnt, ch);
done = ch == '#';
}
gotSigio = 0;
}
}
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1)
errExit("tcsetattr");
exit(EXIT_SUCCESS);
}
何时发送『I/O 就绪』信号
终端和伪终端
当产生新的输入时
管道和 FIFO
- 读端:
- 数据写入到管道中(即使已经有未读取的输入存在)
- 管道的写端关闭
- 写端:
- 对管道的读操作增加了管道中的空间大小,因此现在可以写入 PIPE_BUF 个字节而不被阻塞
- 管道的读端关闭
套接字
- Unix 和 Internet 域下的数据报套接字
- 一个输入数据报达到套接字(即使已经有未读取的数据报正等待读取)
- 套接字上发生了异步错误
- Unix 和 Internet 域下的流式套接字
- 监听套接字上接收到新的连接
- TCP
connect()
请求完成,也就是 TCP 连接的主动端进入ESTABLISHED
状态。 - 套接字上接收到了新的输入(即使已经有未读取的输入存在)
- 套接字对端使用
showdown()
关闭了写连接(半关闭),或者通过close()
完全关闭 - 套接字上输出就绪
- 套接字上发生了异步错误
inotify 文件描述符
当 inotify 文件描述符称为可读状态时会产生一个信号——也就是由 inotify 文件描述符监视的其中一个文件上有事件发生时。
epoll 编程接口
epoll
API 的主要优点如下:
- 当检查大量的文件描述符时,
epoll
的性能延展性比select()
和poll()
高很多。 epoll
API 既支持水平触发也支持边缘触发。与之相反,select()
和poll()
只支持水平触发,而信号驱动 I/O 只支持边缘触发。- 可以避免复杂的信号处理流程(比如信号队列溢出时的处理)。
- 灵活性高,可以指定我们希望检查的事件类型(例如,检查套接字文件描述符的读就绪,写就绪或者两者同时指定)。
epoll
API 的核心数据结构称作 epoll
实例,它和一个打开的文件描述符相关联。这个文件描述符不是用来做 I/O 操作的,相反,它是内核数据结构的句柄,这些内核数据结构实现了两个目的。
- 记录了在进程中声明过的感兴趣的文件描述符列表 —— interest list (兴趣列表)
- 维护了处于 I/O 就绪态的文件描述符列表 —— ready list (就绪列表)
ready list 中的成员是 interest list 的子集。
epoll
API 由以下 3 个系统调用组成:
- 系统调用
epoll_create()
创建一个epoll
实例,返回代表该实例的文件描述符。 - 系统调用
epoll_ctl()
操作同epoll
实例相关联的兴趣列表。通过epoll_ctl()
,我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码。 - 系统调用
epoll_wait()
返回与epoll
实例相关联的就绪列表中的成员。
创建 epoll
实例: epoll_create()
系统调用 epoll_create()
创建了一个新的 epoll
实例,其对应的兴趣列表初始化为空。
#include <sys/epoll.h>
int epoll_create(int size);
// Return file descriptor on success, or -1 on error
// size 指定想要检查的文件描述符个数,该参数并不是上限,而是告诉内核应该如何为内部数据结构划分初始大小
作为函数返回值,epoll_create()
返回了代表新创建的 epoll()
实例的文件描述符。这个文件描述符在其他几个 epoll
系统调用中用来表示 epoll
实例。当这个文件描述符不再需要时,应该通过 close()
来关闭。当所有与 epoll
实例相关的文件描述符都背关闭时,实例被销毁,相关的资源都返还给系统。
修改 epoll
的兴趣列表: epoll_ctl()
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
// return 0 on success, or -1 on error
-
fd
指定要修改的文件描述符,它甚至可以是另一个epoll
实例的文件描述符,但是,这里fd
不能作为普通文件或目录的文件描述符 -
op
指定需要执行的操作:EPOLL_CTL_ADD
,加入兴趣列表中EPOLL_CTL_MOD
,修改 fd 上设定的时间,需要用到由 ev 所指向的结构体中的信息EPOLL_CTL_DEL
,移除兴趣列表
-
ev
是指向结构体epoll_even
的指针,结构体定义如下:struct epoll_event { uint32_t event; /* epoll events (bit mask) */ epoll_data_t data; /* User data */ };
其中 data 的字段如下
typedef union epoll_data { void *ptr; /* Pointer to user-defined data */ int fd; /* File descriptor */ uint32_t u32; /* 32-bit integer */ uint64_t u64; /* 64-bit integer */ };
参数
ev
为文件描述符fd
所做的设置如下:- 结构体
epoll_event
中的events
字段是一个位掩码,它指定了我们为待检查的描述符fd
上感兴趣的事件集合。 data
字段是一个联合体,当描述符fd
稍后称为就绪态时,联合体的成员可用来指定传回给调用进程的信息。
- 结构体
使用 epoll_create()
和 epoll_ctl()
int epfd;
struct epoll_event ev;
epfd = epoll_create(5);
if (epfd == -1)
errExit("epoll_create");
ev.data.fd = fd;
ev.events = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
事件等待:epoll_wait()
系统调用 epoll_wait()
返回 epoll
实例中处于就绪态的文件描述符信息。单个 epoll_wait()
调用能返回多个就绪态文件描述符的信息。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
// Return number of ready file descriptor, 0 on timeout, or -1 on error
参数 evlist
所指向的结构体数组重返回的是有关就绪态文件描述符的信息。数组evlist
的空间由调用者负责申请,所包含的袁术个数在参数 maxevents
中指定。
在数组 evlist
中,每个元素返回的都是单个就绪态文件描述符的信息。events
字段返回了在该描述符上一届发生的事件掩码。data
字段返回的是我们在描述符下使用 epoll_ctl
注册感兴趣的事件时在 ev.data
中所指定的值。注意,data
字段是唯一一个可获知同这个事件相关的文件描述符号的途径。因此,当我面调用 epoll_ctl()
将文件描述符添加到兴趣列表中时,应该要么将 ev.data.fd
设为文件描述符号,要么将 ev.data.ptr
设为指定包含文件描述符号的结构体。
参数 timeout
用来确定 epoll_wait()
的阻塞行为,有如下几种:
- -1: 一直阻塞
- 0: 执行一次非阻塞式检查
- >0: 调用将阻塞至多 timeout 毫秒,直到文件描述符上有事件发送,或者直到捕捉到一个信号为止
epoll
事件
当我们调用 epoll_ctl()
时可以在 ev.events
中指定的位掩码以及由 epoll_wait()
返回的 evlist[].events
中的值在下表给出:
epoll
中 events
字段的位掩码值
位掩码 | 作为 epoll_ctl() 的输入? |
作为 epoll_wait() 返回 |
描述 |
---|---|---|---|
EPOLLIN | √ | √ | 可读取非高优先级的数据 |
EPOLLPRI | √ | √ | 可读取高优先级的数据 |
EPOLLRDHUP | √ | √ | 套接字对端关闭 |
EPOLLOUT | √ | √ | 普通数据可写 |
EPOLLET | √ | 采用边缘触发事件通知 | |
EPOLLONESHOT | √ | 在完成事件通知之后禁用检查 | |
EPOLLERR | √ | 有错误发生 | |
EPOLLHUP | √ | 出现挂断 |
EPOLLONESHOT
标志
默认情况下,一旦通过 epoll_ctl()
的 EPOLL_CTL_ADD
操作将文件描述符添加到 epoll
实例的兴趣列表中后,它会保持激活状态(即,之后对 epoll_wait()
的调用会在描述符处于就绪态时通知我们) 直到我们显示地通知 epoll_ctl()
的 EPOLL_CTL_DEL
操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给 epoll_ctl()
的 ev.events
中指定 EPOLLONESHOT
标志。如果指定了这个标志,那么在下一个 epoll_wait()
调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活态,之后的 epoll_wait()
调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后通过 epoll_ctl()
的 EPOLL_CTL_MOD
操作重新激活对这个文件描述符的检查。
示例程序
#include <sys/epoll.h>
#include <fcntl.h>
#include "tlpi_hdr.h"
#define MAX_BUF 1000 /* Maximum bytes fetched by a single read() */
#define MAX_EVENTS 5 /* Maximum number of events to be returned from a signle epoll_wait() call */
int
testEpoll(int argc, char *argv[])
{
int epfd, ready, fd, s, j, numOpenFds;
struct epoll_event ev;
struct epoll_event evlist[MAX_EVENTS];
char buf[MAX_BUF];
if (argc < 2 || strcmp(argv[1], "--help") == 0)
usageErr("%s file ...\n", argv[0]);
// 创建一个 epoll 实例
epfd = epoll_create(argc - 1);
if (epfd == -1)
errExit("epoll_create");
// 打开由命令行参数指定的每个文件,以此作为输入
for (int j = 1; j < argc; j++) {
fd = open(argv[j], O_RDONLY);
if (fd == -1)
errExit("open");
printf("Opened \"%s\" on fd %d\n", argv[j], fd);
ev.events = EPOLLIN; /* Only interested in input events */
ev.data.fd = fd;
// 将得到的文件描述符添加到 epoll 实例的兴趣列表中
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) == -1)
errExit("epoll_ctl");
}
numOpenFds = argc - 1;
// 执行一个循环
while (numOpenFds > 0) {
/* Fetch up to MAX_EVENTS items from the ready list */
printf("About to epoll_wait()\n");
// 循环中调用 epoll_wait() 来检查 epoll 实例的兴趣列表中的文件描述符,
// 并处理每个调用返回的事件
ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if (ready == -1) {
// 被信号打断处理
if (errno == EINTR)
continue;
else
errExit("epoll_wait");
}
printf("Ready: %d\n", ready);
/* Deal with returned list of events */
// 如果 epoll_wait() 调用成功,程序就再执行一个内层循环检查 evlist
// 中每个已就绪的元素。
for (int j = 0; j < ready; j++) {
printf(" fd=%d; events: %s%s%s\n", evlist[j].data.fd,
// 对于 evlist 中的每个元素,程序不只是检查 events 字段中的 EPOLLIN 标记
(evlist[j].events & EPOLLIN) ? "EPOLLIN" : "",
// EPOLLHUP、EPOLLERR 也要检查
(evlist[j].events & EPOLLHUP) ? "EPOLLHUP" : "",
(evlist[j].events & EPOLLERR) ? "EPOLLERR" : "");
if (evlist[j].events & EPOLLIN) {
s = read(evlist[j].data.fd, buf, MAX_BUF);
if (s == -1)
errExit("read");
printf(" read %d bytes: %.*s\n", s, s, buf);
} else if (evlist[j].events & (EPOLLHUP | EPOLLERR)) {
/* If EPOLLIN and EPOLLHUP were both set, then there might
* be more than MAX_BUF bytes to read. Therefore, we close
* the file descriptor only if EPOLLIN was not set.
* We'll read further bytes after the next epoll_wait(). */
// 当所有打开的文件描述符都关闭后,循环终止
printf(" closing fd %d\n", evlist[j].data.fd);
if (close(evlist[j].data.fd) == -1)
errExit("close");
numOpenFds--;
}
}
}
printf("All file descriptors closed; bye\n");
exit(EXIT_SUCCESS);
}
深入探究 epoll
的语义
当我面通过 epoll_create()
创建一个 epoll
实例时,内核在内存中创建了一个新的 i-node
并打开文件描述(文件描述符表示的是一个打开文件的上下文信息(大小、内容、编码等与文件有关的信息),这部分实际是由内核来维护的。),随后在调用进程中为打开的这个文件描述分配一个新的文件描述符。同 epoll
实例的兴趣列表相关联的是打开的文件描述,而不是 epoll
文件描述符。这将产生下列结果:
- 如果我们使用
dup()
(或类似的函数)复制一个epoll
文件描述符,那么被复制的描述符所指代的epoll
兴趣列表和就绪列表同原始的epoll
文件描述符相同。若要修改兴趣列表,在epoll_ctl()
的参数epfd
上设定文件描述符可以是原始的也可以是复制的。 - 上条观点同意也适用于
fork()
调用之后的情况。此时子进程通过继承复制了父进程的epoll
文件描述符,而这个复制的文件描述符所指向的epoll
数据结果同原始的描述符相同。
当我们执行 epoll_ctl()
的 EPOLL_CTL_ADD
操作时,内核在 epoll
兴趣列表中添加了一个元素,这个元素同时记录了需要检查的文件描述符数量以及对应的打开文件描述的引用。 epoll_wait()
调用的目的就是让内核负责监视打开的文件描述符。这表示我们必须对之前的观点做改进:如果一个文件描述符是 epoll
兴趣列表中的成员,当关闭它后会自动从列表中删除。改进版应该是这样的:一旦所有指向打开文件描述的文件描述符都被关闭后,这个打开的文件描述符将从epoll
的兴趣列表中移除。这表示如果我们通过 dup()
(或类似的函数)或者 fork()
为打开的文件创建了描述符副本,那么这个打开的文件只会在原始的描述符以及所有其他的副本都被关闭时才会移除。
epoll
同 I/O 多路复用的性能对比
poll()
、select()
以及 epoll
进行 100000 次监视操作所花费的时间
被监视的文件描述符数量(N) | poll() 所占用的 CPU 时间(秒) |
select() 所占用的 CPU 时间(秒) |
epoll 所占用的 CPU 时间(秒) |
---|---|---|---|
10 | 0.61 | 0.73 | 0.41 |
100 | 2.9 | 3.0 | 0.42 |
1000 | 35 | 35 | 0.53 |
10000 | 990 | 930 | 0.66 |
为什么:
- 每次调用
select()
和poll()
时,内核必须检查所有在调用中指定的文件描述符。与之相反,当通过epoll_ctl()
指定了需要监视的文件描述符时,内核会在与打开的文件描述上下文相关联的列表中记录该描述符。之后每当执行 I/O 操作使得文件描述符成为就绪态时,内核就在epoll
描述符的就绪列表中添加一个元素。(单个打开的文件描述上下文中一次 I/O 事件可能导致与之相关的多个文件描述符成为就绪态。)之后的epoll_wait()
调用从就绪列表中简单地取出这些元素。 - 每次调用
select()
或poll()
时,我传递一个标记了所有待监视的文件描述符的数据结构给内核,调用返回时,内核将所有标记为就绪态的文件描述符的数据结构再传回给我们。与之相反,在epoll
中我们使用epoll_ctl()
在内核控件中建立一个数据结构,该数据结构会将待监视的文件描述符都记录下来。一旦这个数据结构建立完成,稍后每次调用epoll_wait()
时就不需要再传递任何与文件描述符有关的信息给内核了,而调用返回的信息中只包含那些已经处于就绪态的描述符。
边缘触发通知
epoll
API 还能以边缘触发方式进行通知——也就是说,会告诉我们自从上一次调用 epoll_wait()
以来文件描述符上是否已经有 I/O 活动了(或者由于描述符被打开了,如果之前没有调用的话)。使用 epoll
的边缘触发通知在语义上类似于信号驱动 I/O ,只是如果有多个 I/O 事件发生的话,epoll
会将它们合并成一次单独的通知,通过 epoll_wait()
返回,而再信号驱动 I/O 中则可能会产生多个信号。
采用 epoll
的边缘触发通知机制的程序基本框架如下:
- 让所有监视的文件描述符都称为非阻塞的。
- 通过
epoll_ctl()
构建epoll
的兴趣列表。 - 通过如下的循环处理 I/O 事件:
- 通过
epoll_wait()
取得处于就绪态的描述符列表 - 针对每一个处于就绪态的文件描述符,不断进行 I/O 处理知道相关的系统调用(例如
read(), write(), recv(), send()
或accept()
)返回EAGAIN
或EWOULDBLOCK
错误。
- 通过
当采用边缘触发通知时避免出现文件描述符饥饿现象
假设其中一个就绪态文件描述符又大量输入,如果使用非阻塞式读操作将所有输入都读取,那么此时就会有使其他的文件描述符处于饥饿状态的风险存在(即,在我们再次检查这些文件描述符是否处于就绪态并执行 I/O 操作前会有很长的一段处理时间)。该问题的一个解决方案是让应用程序维护一个列表,列表中存放着已经被通知为就绪态的文件描述符。通过一个循环按照如下方式不断处理。
- 调用
epoll_wait()
监视文件描述符,并将处于就绪态的描述符添加到应用程序维护的列表中。如果这个文件描述符已经注册到应用程序维护的列表中了,那么这次监视操作的超时时间应该设为较小的值或者是0。这样如果没有新的文件描述符成为就绪态,应用程序就可以迅速进行到下一步,去处理那些已经处于就绪态的文件描述符了。 - 在应用程序维护的列表中,只在那些已经注册为就绪态的文件描述符上进行一定限度的 I/O 操作(可能是以轮转调度(round-robin)方式循环处理,而不是每次
epoll_wait()
调用后从列表头开始处理 )。当相关的非阻塞 I/O 系统调用出现EAGAIN
或EWOULDBLOCK
错误时,文件描述符就可以在应用程序维护的列表中移除了。
参考资料
《Linux/Unix 系统编程手册》
【转载】Python的GIL是什么鬼,多线程性能究竟如何
前言:博主在刚接触Python的时候时常听到GIL这个词,并且发现这个词经常和Python无法高效的实现多线程划上等号。本着不光要知其然,还要知其所以然的研究态度,博主搜集了各方面的资料,花了一周内几个小时的闲暇时间深入理解了下GIL,并归纳成此文,也希望读者能通过次本文更好且客观的理解GIL。
文章欢迎转载,但转载时请保留本段文字,并置于文章的顶部
作者:卢钧轶(cenalulu)
本文原文地址:http://cenalulu.github.io/python/gil-in-python/
GIL是什么
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL
。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL
并不是Python的特性,Python完全可以不依赖于GIL
那么CPython实现中的GIL又是什么呢?GIL
全称Global Interpreter Lock
为了避免误导,我们还是来看一下官方给出的解释:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
好吧,是不是看上去很糟糕?一个防止多线程并发执行机器码的一个Mutex,乍一看就是个BUG般存在的全局锁嘛!别急,我们下面慢慢的分析。
为什么会有GIL
由于物理上得限制,各CPU厂商在核心频率上的比赛已经被多核所取代。为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。即使在CPU内部的Cache也不例外,为了有效解决多份缓存之间的数据同步时各厂商花费了不少心思,也不可避免的带来了一定的性能损失。
Python当然也逃不开,为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。
慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,本且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
所以简单的说GIL的存在更多的是历史原因。如果推到重来,多线程的问题依然还是要面对,但是至少会比目前GIL这种方式会更优雅。
GIL的影响
从上文的介绍和官方的定义来看,GIL无疑就是一把全局排他锁。毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。
那么读者就会说了,全局锁只要释放的勤快效率也不会差啊。只要在进行耗时的IO操作的时候,能释放GIL,这样也还是可以提升运行效率的嘛。或者说再差也不会比单线程的效率差吧。理论上是这样,而实际上呢?Python比你想的更糟。
下面我们就对比下Python在多线程和单线程下得效率对比。测试方法很简单,一个循环1亿次的计数器函数。一个通过单线程执行两次,一个多线程执行。最后比较执行总时间。测试环境为双核的Mac pro。注:为了减少线程库本身性能损耗对测试结果带来的影响,这里单线程的代码同样使用了线程。只是顺序的执行两次,模拟单线程。
顺序执行的单线程(single_thread.py)
#! /usr/bin/python
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
t.join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
同时执行的两个并发线程(multi_thread.py)
#! /usr/bin/python
from threading import Thread
import time
def my_counter():
i = 0
for _ in range(100000000):
i = i + 1
return True
def main():
thread_array = {}
start_time = time.time()
for tid in range(2):
t = Thread(target=my_counter)
t.start()
thread_array[tid] = t
for i in range(2):
thread_array[i].join()
end_time = time.time()
print("Total time: {}".format(end_time - start_time))
if __name__ == '__main__':
main()
下图就是测试结果
可以看到python在多线程的情况下居然比单线程整整慢了45%。按照之前的分析,即使是有GIL全局锁的存在,串行化的多线程也应该和单线程有一样的效率才对。那么怎么会有这么糟糕的结果呢?
让我们通过GIL的实现原理来分析这其中的原因。
当前GIL设计的缺陷
基于pcode数量的调度方式
按照Python社区的想法,操作系统本身的线程调度已经非常成熟稳定了,没有必要自己搞一套。所以Python的线程就是C语言的一个pthread,并通过操作系统调度算法进行调度(例如linux是CFS)。为了让各个线程能够平均利用CPU时间,python会计算当前已执行的微代码数量,达到一定阈值后就强制释放GIL。而这时也会触发一次操作系统的线程调度(当然是否真正进行上下文切换由操作系统自主决定)。
伪代码:
while True:
acquire GIL
for i in 1000:
do something
release GIL
/* Give Operating System a chance to do thread scheduling */
这种模式在只有一个CPU核心的情况下毫无问题。任何一个线程被唤起时都能成功获得到GIL(因为只有释放了GIL才会引发线程调度)。但当CPU有多个核心的时候,问题就来了。从伪代码可以看到,从release GIL到acquire GIL之间几乎是没有间隙的。所以当其他在其他核心上的线程被唤醒时,大部分情况下主线程已经又再一次获取到GIL了。这个时候被唤醒执行的线程只能白白的浪费CPU时间,看着另一个线程拿着GIL欢快的执行着。然后达到切换时间后进入待调度状态,再被唤醒,再等待,以此往复恶性循环。
PS:当然这种实现方式是原始而丑陋的,Python的每个版本中也在逐渐改进GIL和线程调度之间的互动关系。例如先尝试持有GIL在做线程上下文切换,在IO等待时释放GIL等尝试。但是无法改变的是GIL的存在使得操作系统线程调度的这个本来就昂贵的操作变得更奢侈了。
关于GIL影响的扩展阅读
为了直观的理解GIL对于多线程带来的性能影响,这里直接借用的一张测试结果图(见下图)。图中表示的是两个线程在双核CPU上得执行情况。两个线程均为CPU密集型运算线程。绿色部分表示该线程在运行,且在执行有用的计算,红色部分为线程被调度唤醒,但是无法获取GIL导致无法进行有效运算等待的时间。
由图可见,GIL的存在导致多线程无法很好的立即多核CPU的并发处理能力。
那么Python的IO密集型线程能否从多线程中受益呢?我们来看下面这张测试结果。颜色代表的含义和上图一致。白色部分表示IO线程处于等待。可见,当IO线程收到数据包引起终端切换后,仍然由于一个CPU密集型线程的存在,导致无法获取GIL锁,从而进行无尽的循环等待。
简单的总结下就是:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
如何避免受到GIL的影响
说了那么多,如果不说解决方案就仅仅是个科普帖,然并卵。GIL这么烂,有没有办法绕过呢?我们来看看有哪些现成的方案。
用multiprocessing替代Thread
multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。具体难点在哪有兴趣的读者可以扩展阅读这篇文章
用其他解析器
之前也提到了既然GIL只是CPython的产物,那么其他解析器是不是更好呢?没错,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。毕竟功能和性能大家在初期都会选择前者,Done is better than perfect。
所以没救了么?
当然Python社区也在非常努力的不断改进GIL,甚至是尝试去除GIL。并在各个小版本中有了不少的进步。有兴趣的读者可以扩展阅读这个Slide
另一个改进Reworking the GIL
- 将切换颗粒度从基于opcode计数改成基于时间片计数
- 避免最近一次释放GIL锁的线程再次被立即调度
- 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)
总结
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结:
- 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
- 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
- GIL在较长一段时间内将会继续存在,但是会不断对其进行改进
Reference:
ElasticSearch基础
增删查改范例
Restful vs Java API
Java API 说明
需要添加依赖:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.6.4</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.9.1</version>
</dependency>
需要获取 client 实例:
Settings settings = Settings.EMPTY;
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getLocalHost(), 9300));
增
Restful
curl -XPOST 'localhost:9200/books/es/1' -d '{"title": "Elasticsearch Server", "published": 2013}'
返回:
{"_index":"books","_type":"es","_id":"1","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"created":true}
Java API
// add,可以直接使用 json 添加,也可以像下面一样使用
XContentBuilder builder = XContentFactory.jsonBuilder().startObject()
.field("title", "Mastering Elasticsearch").field("published", "2013").endObject();
String json = builder.string();
System.out.println(json);
// IndexResponse indexResponse = client.prepareIndex("books", "es", "2")
// .setSource(json, XContentType.JSON).get();
IndexResponse indexResponse = client.prepareIndex("books", "es", "2")
.setSource(builder).get();
System.out.println(indexResponse);
/**
结果:
{"title":"Mastering Elasticsearch","published":"2013"} // JSON
IndexResponse[index=books,type=es,id=2,version=1,result=created,shards={"total":2,"successful":1,"failed":0}] // indexResponse
**/
查
Restful
curl -XGET 'localhost:9200/books/_search?pretty'
{
"took" : 41,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 1.0,
"hits" : [
{
"_index" : "books",
"_type" : "es",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"title" : "Mastering Elasticsearch",
"published" : 2013
}
},
{
"_index" : "books",
"_type" : "es",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "Elasticsearch Server",
"published" : 2013
}
},
{
"_index" : "books",
"_type" : "solr",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "Apache Solr 4 Cookbook",
"published" : 2012
}
}
]
}
}
curl -XGET 'localhost:9200/books/_search?pretty&q=title:elasticsearch'
{
"took" : 12,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 0.7373906,
"hits" : [
{
"_index" : "books",
"_type" : "es",
"_id" : "1",
"_score" : 0.7373906,
"_source" : {
"title" : "Elasticsearch Server",
"published" : 2013
}
},
{
"_index" : "books",
"_type" : "es",
"_id" : "2",
"_score" : 0.25811607,
"_source" : {
"title" : "Mastering Elasticsearch",
"published" : 2013
}
}
]
}
}
Java API
通过 id 查询
// get by id
GetResponse response = client.prepareGet("books", "es", "2").get();
System.out.println(response);
/**
结果:
{"_index":"books","_type":"es","_id":"2","_version":1,"found":true,"_source":{"title":"Mastering Elasticsearch","published":"2013"}}
**/
通过 query 查询
SearchResponse response = client.prepareSearch("books")
.setQuery(
QueryBuilders.termsQuery("title","elasticsearch")
)
.get();
System.out.println(response);
/**
结果
{"took":11,"timed_out":false,"_shards":{"total":5,"successful":5,"skipped":0,"failed":0},"hits":{"total":2,"max_score":0.7373906,"hits":[{"_index":"books","_type":"es","_id":"1","_score":0.7373906,"_source":{"title": "Elasticsearch Server", "published": 2013}},{"_index":"books","_type":"es","_id":"2","_score":0.25811607,"_source":{"title":"Mastering Elasticsearch","published":"2013"}}]}}
**/
改
Restful
更新索引中的文档是一项复杂的任务。在内部,ElasticSearch 必须首先后去文档,从 _source
属性获得数据,删除旧的文件,更改 _source
属性,然后把它作为新的文档来索引。它如此复杂,因为信息一旦在 Lucene 的倒排索引中存储,就不能再被更改。
curl -XPOST 'localhost:9200/books/es/2/_update' -d '{"script": "ctx._source.published = \"2017\"" }'
{"_index":"books","_type":"es","_id":"2","_version":2,"result":"updated","_shards":{"total":2,"successful":1,"failed":0}}
结果验证:
curl -XGET 'localhost:9200/books/_search?pretty&q=title:mastering'
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.25811607,
"hits" : [
{
"_index" : "books",
"_type" : "es",
"_id" : "2",
"_score" : 0.25811607,
"_source" : {
"title" : "Mastering Elasticsearch",
"published" : "2017"
}
}
]
}
}
Java API
// use prepareUpdate
UpdateResponse response = client.prepareUpdate("books", "es", "2").setScript(
new Script("ctx._source.published = \"2017\"")
).get();
UpdateResponse response = client.prepareUpdate("books", "es", "2")
.setDoc(
XContentFactory.jsonBuilder().startObject().field("published", "2017").endObject()
)
.get();
// use UpdateRequest
UpdateResponse response = client.update(
new UpdateRequest("books", "es", "2")
.doc(
XContentFactory.jsonBuilder().startObject().field("published", "2017").endObject()
)
).get();
System.out.println(response);
/**
结果:
UpdateResponse[index=books,type=es,id=2,version=2,result=updated,shards=ShardInfo{total=2, successful=1, failures=[]}]
**/
删
Restful
curl -XDELETE 'localhost:9200/books/es/2'
{"found":true,"_index":"books","_type":"es","_id":"2","_version":3,"result":"deleted","_shards":{"total":2,"successful":1,"failed":0}}
结果验证:
curl -XGET 'localhost:9200/books/_search?pretty&q=title:mastering'
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 0,
"max_score" : null,
"hits" : [ ]
}
}
Java API
DeleteResponse response = client.prepareDelete("books", "es", "2").get();
System.out.println(response);
/**
结果:
DeleteResponse[index=books,type=es,id=2,version=5,result=deleted,shards=ShardInfo{total=2, successful=1, failures=[]}]
**/
基础概念
数据架构的主要概念
-
索引
索引(index) 是 Elasticsearch 对逻辑数据的逻辑存储,所以它可以分为更小的部分。你可以把索引看成关系型数据库的表。然而,索引的结构是为快速有效的全文索引准备的,特别是它不存储原始值。Elasticsearch 可以把索引存放在一台机器或者分散在多台服务器上,每个索引有一或多个分片(shard),每个分片可以有多个副本(replica)。
-
文档
存储在 Elasticsearch 中的主要实体叫文档( document)。用关系型数据库来类比的话,一个文档相当于数据库表中的一行记录。
文档由多个字段组成,每个字段可能多次出现在一个文档里,这样的字段叫多值字段(multivalued)。每个字段有类型,如文本、数值、日期等。字段类型也可以是复杂类型,一个字段包含其他子文档或者数组。
每个文档存储在一个索引中并有一个 Elasticsearch 自动生成的唯一标识符和文档类型。文档需要有对应文档类型的唯一标识符,这意味着在一个索引中,两个不同类型的文档可以有相同的唯一标识符。
-
文档类型
在 Elasticsearch 中,一个索引对象可以存储很多不同用途的对象。例如,一个博客应用程序可以保存文章和评论。文档类型让我们轻易地区分单个索引中的不同对象。每个文档可以有不同的结构,但在实际部署中,将文件按类型区分对数据操作有很大帮助。当然,需要记住一个限制,不同的文档类型不能为相同的属性设置 不同的类型。例如,在同一索引中的所有文档类型中,一个叫 title 的字段必须具有相同的类型。
-
映射
文档中的每个字段都必须根据不同类型做相应的分析。Elasticsearch 在映射中存储有关字段的信息。每一个 文档类型都有自己的映射,即使我们没有明确定义。
主要概念
-
节点和集群
Elasticsearch 可以运行在许多互相合作的服务器上。这些服务器称为集群(cluster),形成集群的每个服务器称为节点(node)。
-
分片
当有大量的文档时,由于内存的限制、硬盘能力、处理能力不足、无法足够快地响应客户端请求等,一个节点可能不够。在这种情况下,数据可以分为较小的称为分片(shard)的部分(其中每个分片都是一个独立的ApacheLucene 索引)。每个分片可以放在不同的服务器上,因此,数据可以在集群的节点中传播。当你查询的索引分布在多个分片上时,Elasticsearch 会把查询发送给每个相关的分片,并将结果合并在一起,而应用程序并不知道分片的存在。此外,多个分片可以加快索引。
-
副本
为了提高查询吞吐量或实现高可用性,可以使用分片副本。副本(replica)只是一个分片的精确复制,每个分片可以有零个或多个副本。换句话说,Elasticsearch 可以有许多相同的分片,其中之一被自动选择去更改索引操作。这种特殊的分片称为主分片(primaryshard),其余称为副本分片(replicashard)。在主分片丢失时,例如该分片数据所在服务器不可用,集群将副本提升为新的主分片。
基本查询
词条查询(term
)
词条查询是 Elasticsearch 中的一个简单查询。它仅匹配在给定字段中含有该词条的文档,而且是确切的、未经分析的词条。
{
"query" : {
"term" : {
"title": "elasticsearch"
}
}
}'
# 另外的查询 json
{
"query" : {
"term" : {
"title": {
"value": "elasticsearch",
"boost": 10.0
}
}
}
}'
由于索引内容全改为小写,所以搜索的也改为小写
多词条查询(terms-tags
)
{
"query" : {
"terms" : {
"published" : [ "2013", "2012" ]
}
}
}
匹配所有文件(match_all
)
{
"query" : {
"match_all": {}
}
}
常用词(common
)
常用词查询是在没有使用停用词(stopword,http://en.wikipedia.org/wiki/Stop_words)的情况下,Elasticsearch为了提高常用词的查询相关性和精确性而提供的一个现代解决方案。例如,“book and coffee”可以翻译成3个词查询,每一个都有性能上的成本(词越多,查询性能越低)。**但『and』这个词非常常见,对文档得分的影响非常低。解决办法是常用词查询,将查询分为两组。第一组包含重要的词,出现的频率较低。第二组包含较高频率的、不那么重要的词。**先执行第一个查询,Elasticsearch从第一组的所有词中计算分数。这样,通常都很重要的低频词总是被列入考虑范围。然后,Elasticsearch对第二组中的词执行二次查询,但只为与第一个查询中匹配的文档计算得分。这样只计算了相关文档的得分,实现了更高的性能。
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-common-terms-query.html
{
"query": {
"common": {
"title": {
"query": "this is bonsai cool",
"cutoff_frequency": 0.001
}
}
}
}
查询可以使用下列的参数:
- query:定义实际查询内容
- cutoff_frequency:这个参数定义一个百分比(0.001表示0.1%)或一个绝对值(当此属性值>=1时)。这个值用来构建高、低频词组。此参数设置为0.001意味着频率<=0.1%的词将出现在低频词组中。
- low_freq_operator:这个参数可以设为 or 或 and,默认是or。它用来指定为低频词组构建查询时用到的布尔运算符。如果希望所有的词都在文档中出现才认为是匹配,应该把它设置为and。
- high_freq_operator:这个参数可以设为 or 或a nd,默认是or。它用来指定为高频词组构建查询时用到的布尔运算符。如果希望所有的词都在文档中出现才认为是匹配,那么应该把它设置为and。
- minimum_should_match:不使用 low_freq_operator 和 high_freq_operator 参数的话,可以使用使用minimum_should_match 参数。和其他查询一样,它允许指定匹配的文档中应该出现的查询词的最小个数。
- boost:这个参数定义了赋给文档得分的加权值。
- analyzer:这个参数定义了分析查询文本时用到的分析器名称。默认值为 default analyzer。
- disable_coord:此参数的值默认为 false,它允许启用或禁用分数因子的计算,该计算基于文档中包含的所有查询词的分数。把它设置为true,得分不那么精确,但查询将稍快。
match 查询(match
)
match 查询把 query 参数中的值拿出来,加以分析,然后构建相应的查询**。使用 match 查询时,Elasticsearch 将对一个字段选择合适的分析器,所以可以确定,传给 match 查询的词条将被建立索引时相同的分析器处理**。请记住,match 查询(以及将在稍后解释的 multi_match 查询)不支持 Lucene 查询语法。但是,它是完全符合搜索需求的一个查询处理器。
最简单的查询
{
"query": {
"match" : {
"message" : "this is a test"
}
}
}
下面我们看看它几种类型
布尔值
{
"query": {
"match" : {
"message" : {
"query": "this is a test",
"operator": "and"
}
}
}
}
布尔匹配查询分析提供的文本,然后做出布尔查询。有几个参数允许控制布尔查询匹配行为:
- operator:此参数可以接受 or 和 and,控制用来连接创建的布尔条件的布尔运算符。默认值是or。如果希望查询中的所有条件都匹配,可以使用and运算符。
- analyzer:这个参数定义了分析查询文本时用到的分析器的名字。默认值为 default analyzer。
- fuzziness:可以通过提供此参数的值来构建模糊查询(fuzzy query)。它为字符串类型提供从0.0到1.0的值。构造模糊查询时,该参数将用来设置相似性。
- prefix_length:此参数可以控制模糊查询的行为。
- max_expansions:此参数可以控制模糊查询的行为。
- zero_terms_query:该参数允许**指定当所有的词条都被分析器移除时(例如,因为停止词),查询的行为。它可以被设置为none或all,默认值是none。**在分析器移除所有查询词条时,该参数设置为none,将没有文档返回;设置为all,则将返回所有文档。
- cutoff_frequency:该参数允许将查询分解成两组:一组低频词和一组高频词。
match_phrase
{
"query": {
"match_phrase" : {
"message" : {
"query" : "this is a test",
"analyzer" : "my_analyzer"
}
}
}
}
match_phrase 查询类似于布尔查询,不同的是,它从分析后的文本中构建短语查询,而不是布尔子句。该查询可以使用下面几种参数:
- slop:这是一个整数值,该值定义了文本查询中的词条和词条之间可以有多少个未知词条,以被视为跟一个短语匹配。此参数的默认值是0,这意味着,不允许有额外的词条1。
- analyzer:这个参数定义定义了分析查询文本时用到的分析器的名字。默认值为 default analyzer。
match_phrase_prefix
{
"query": {
"match_phrase_prefix" : {
"message" : {
"query" : "quick brown f",
"max_expansions" : 10
}
}
}
}
match_query 查询的最后一种类型是 match_phrase_prefix 查询。此查询跟 match_phrase 查询几乎一样,但除此之外,它允许查询文本的最后一个词条只做前缀匹配。此外,除了 match_phrase 查询公开的参数,还公开了一个额外参数max_expansions。这个参数控制有多少前缀将被重写成最后的词条。
multi_match
可以通过 fields 指定多字段进行 match 查询
{
"query": {
"multi_match" : {
"query": "this is a test",
"fields": [ "subject", "message" ]
}
}
}
除了 match 提供的参数,它还可以通过以下参数来控制行为:
- use_dis_max:该参数定义一个布尔值,设置为 true 时,使用析取最大分查询,设置为false时,使用布尔查询。默认值为 true。
- tie_breaker:只有在 use_dis_max 参数设为 true 时才会使用这个参数。它指定低分数项和最高分数项之间的平衡。
query_string 查询
相比其他查询, query_string 支持全部 Apache Lucene 查询语法
{
"query": {
"query_string" : {
"query" : "city.\\*:(this AND that OR thus)"
}
}
}
针对多字段
将 use_ids_max
设置为 true
simple_query_string 查询
使用 Lucene 最新查询解析器之一: SimpleQueryParser
{
"query": {
"simple_query_string" : {
"query": "\"fried eggs\" +(eggplant | potato) -frittata",
"fields": ["title^5", "body"],
"default_operator": "and"
}
}
}
标识符查询
仅用于提供的标识符来过滤返回的文档,针对内部 _uid 字段运行
{
"query": {
"ids" : {
"type" : "my_type",
"values" : ["1", "4", "100"]
}
}
}
前缀查询
{
"query": {
"prefix" : { "user" : "ki" }
}
}
fuzzy 查询
基于编辑距离算法来匹配文档。编辑距离的计算基于我们提供的查询词条和被搜索文档。此查询很占用 CPU 资源,但当需要模糊匹配时它很有用,例如,当用户拼写错误时。
{
"query": {
"fuzzy" : {
"user" : {
"value" : "ki",
"boost" : 1.0,
"fuzziness" : 2,
"prefix_length" : 0,
"max_expansions": 100
}
}
}
}
可以使用下面的参数来控制 fuzzy 查询的行为。
- value:此参数指定了实际的查询。
- boost:此参数指定了查询的加权值,默认为1.0。
- min_similarity:此参数指定了一个词条被算作匹配所必须拥有的最小相似度。对字符串字段来说,这个值应该在0到1之间,包含0和1。对于数值型字段,这个值可以大于1,比如查询值是20,min_similarity设为3,则可以得到17~23的值。对于日期字段,可以把min_similarity参数值设为1d、2d、1m等,分别表示1天、2天、1个月。
- prefix_length:此参数指定差分词条的公共前缀长度,默认值为0。
- max_expansions:此参数指定查询可被扩展到的最大词条数,默认值是无限制。
通配符查询
通配符查询允许我们在查询值中使用 * 和 ? 等通配符。此外,通配符查询跟词条查询在内容方面非常类似。
{
"query": {
"wildcard" : { "user" : { "value" : "ki*y", "boost" : 2.0 } }
}
}
more_like_this 查询
more_like_this 查询让我们能够得到与提供的文本类似的文档。Elasticsearch 支持几个参数来定义 more_like_this 查询如何工作,如下所示。
{
"query": {
"more_like_this" : {
"fields" : ["title", "description"],
"like" : "Once upon a time",
"min_term_freq" : 1,
"max_query_terms" : 12
}
}
}
- fields:此参数定义应该执行查询的字段数组,默认值是 _all 字段。
- like_text:这是一个必需的参数,包含用来跟文档比较的文本。
- percent_terms_to_match:此参数定义了文档需要有多少百分比的词条与查询匹配才能认为是类似的,默认值为0.3,意思是30%。
- min_term_freq:此参数定义了文档中词条的最低词频,低于此频率的词条将被忽略,默认值为2。
- max_query_terms:此参数指定生成的查询中能包括的最大查询词条数,默认值为25。值越大,精度越大,但性能也越低。
- stop_words:此参数定义了一个单词的数组,当比较文档和查询时,这些单词将被忽略,默认值为空数组。
- min_doc_freq:此参数定义了包含某词条的文档的最小数目,低于此数目时,该词条将被忽略,默认值为5,意味着一个词条至少应该出现在5个文档中,才不会被忽略。
- max_doc_freq:此参数定义了包含某词条的文档的最大数目,高于此数目时,该词条将被忽略,默认值为无限制。
- min_word_len:此参数定义了单词的最小长度,低于此长度的单词将被忽略,默认值为0。
- max_word_len:此参数定义了单词的最大长度,高于此长度的单词将被忽略,默认值为无限制。
- boost_terms:此参数定义了用于每个词条的加权值,默认值为1。
- boost:此参数定义了用于查询的加权值,默认值为1。
- analyzer:此参数指定了针对我们提供的文本的分析器名称。
范围查询
{
"query": {
"range" : {
"age" : {
"gte" : 10,
"lte" : 20,
"boost" : 2.0
}
}
}
}
- gte:范围查询将匹配字段值大于或等于此参数值的文档。
- gt:范围查询将匹配字段值大于此参数值的文档。
- lte:范围查询将匹配字段值小于或等于此参数值的文档。
- lt:范围查询将匹配字段值小于此参数值的文档。
正则表达式查询
{
"query": {
"regexp":{
"name.first": "s.*y"
}
}
}
版本控制
之前查询返回的结果中有一个 "_version": 1
仔细观察,你会发现在更新相同标识符的文档后,这个版本是递增的。默认情况下,Elasticsearch在添加、更改或删除文档时都会递增版本号。除了告诉我们对文档所做更改的次数,还能够实现**乐观锁**。
Java内存区域与内存溢出异常
图示各区用途
程序计数器
可看作当前线程所执行的字节码的行号指示器。在 JVM 概念模型中(仅概念模型,各实现可能不一致),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器完成。
Java 方法中:正在执行的虚拟机字节码指令的地址
Native 方法中:空(Undefined)
Java 虚拟机栈
线程私有,生命周期与线程相同。
虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame) 用于存储局部变量表(粗浅流行划分中的栈)、操作数栈、动态链接、方法出口等信息。每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
局部变量表:存放编译期可知的各种基本类型、对象引用(不等同于对象本身,可能指向一个对象起始地址的引用指针, 也可能是指向一个代表对象的句柄或其他与此对象相关的位置) 和 returnAddress 类型(指向了一条字节码指令的地址)。
本地方法栈
为虚拟机使用到的 Native 方法服务。
Java 堆
被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例(The heap is the run-time data area from which memory for all class instances and arrays is allocated.)都在这里分配内存。
但随着 JIT 编译器的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,使之不『绝对』了。
Java 堆是 GC 管理的主要区域。从内存回收角度来看,可以将其分为『新生代』(Eden 空间、From Survivor、To Survivor)和『老年代』。从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
Java 堆只需要保证逻辑上连续即可。
通过 -Xmx
(最大堆大小)和 -Xms
(初始堆大小)控制大小。
方法区
各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、JIT 编译后的代码等数据。
被很多人作为『永久代』对待(其实并不是),因为仅仅是之前的 HotSpot 设计团队将其 GC 分代收集扩展至方法区,或者说是使用永久代实现方法区而已。
JVM 规范对方法区的限制非常宽松,除了和 Java 堆一样不需要连续的内存和可以选择固定大小和可扩展外,还可以选择不实现 GC(与此对应,如果实现了则内存回收主要目标是针对常量池的回收和对类型的卸载)。
运行时常量池
方法区的一部分。Class 文件除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后进行方法区的运行时常量池中存放。(这个不是运行时常量池)
运行时长期是区别于上面的 Class 常量池:
-
保存 Class 文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中。
-
相较 Class 文件中的常量池具备动态性,并非预置入 Class 文件中常量池的内容才能进入方法区,运行期间也可能将新的常量放入池中,这种特性被开发人员利用较多的就是
String#intern()
方法(intern详细介绍)
直接内存
不是运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是这部番内存也被频繁使用,且可能会导致 OOM 的出现。
出现原因:1.4 加入 NIO,引入基于 Channel 和 Buffer 的 I/O 方式,可以使用 Native 函数库直接分配堆外内存,然后通过存在 Java 堆中的 DirectByteBuffer
对象作为这块内存的引用进行操作。(避免 Java 堆与 Native 堆复制数据)。
对象探秘
对象创建
-
检查指令是否在常量池中对应到一个类的符号引用,该符号是否被加载、解析和初始化,如果没有则需要执行相应类加载过程。
-
为新生对象分配内存,等同于将一块确定大小的内存从 Java 堆中划分出来。使用方法:
- 指针碰撞:用过的内存放一边,空闲的内存放一边,中间加一个指针作为分界点的指示器,分配仅需将指针移动。
- 空闲列表:维护一个可用的内存块,从中分配。
使用二者是看 Java 堆是否规整决定的。
有一个问题需要考虑,创建对象过于频繁,即使只是修改指针位置也有可能出现线程不安全的情况。实际中 JVM 采用 CAS 配上失败重试的方法保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间进行,即每一个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。线程分配各自的 TLAB,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。使用
-XX:+/-UseTLAB
参数来设定是否使用。 -
JVM 需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用 TLAB,这一工作过程也可以提前至 TLAB 分配时进行。
-
虚拟机要对对象进行必要的设置(对象是哪个类的实例,如何才能找到类的元信息,对象 hash code,对象 GC 分代年龄等信息)。存放于对象头(Object Header)中。
-
执行 new 指令之后会接着执行
<init>
方法,将对象初始化。
对象的内存布局
对象头:
-
存储对象自身的运行时数据,如哈希码、GC分代年龄等。长度在32位和64位的虚拟机中,分别为32bit、 64bit,官方称它为“Mark Word”
-
类型指针,对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
注:如果对象是一个java数组,对象头中还必须有一块记录数据长度的数据。
实例数据
对象真正存储的有用信息,也是程序中定义的各种类型的字段内容。
对齐填充
由于HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,通俗的说,就是对象大小必须是8字节的整数倍。对象头正好是8字节的倍数。当实例数据部分没有对齐时,需要通过对齐填充来补全。
对象的定位访问
建立对象是为了适用对象,Java通过栈上的引用来引用堆中的对象,引用通过何种方式去定位、访问堆中的对象的具体位置取决于虚拟机的实现。目前主流的访问方式有两种:使用句柄和直接指针。
由于 HotSpot 是使用直接指针访问,所以这里指介绍直接指针:
如果是指针访问方式,java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址。
优点:速度相对更快。
关于一个程序的创建过程
package io.github.binglau.jvm;
/**
* 文件描述:
*/
public class ClassCreateProcessTest {
public static B b = new B();
private C c = new C();
static {
System.out.println("A created");
}
public ClassCreateProcessTest() {}
public static void main(String[] args) {
ClassCreateProcessTest a = new ClassCreateProcessTest();
System.out.println(a);
}
}
class B{
static {
System.out.println("B created");
}
//some fields.
}
class C {
static {
System.out.println("C created");
}
//some fields.
}
/**
B created
A created
C created
io.github.binglau.jvm.ClassCreateProcessTest@d716361
**/
binglau@BingLaudeMBP ~/c/c/J/basic> javap -verbose target/classes/io/github/binglau/jvm/ClassCreateProcessTest.class
Classfile /Users/binglau/code/cradle/Java-demo/basic/target/classes/io/github/binglau/jvm/ClassCreateProcessTest.class
Last modified 2017-10-17; size 938 bytes
MD5 checksum 3197a5d18757ab40c96b935f44cba193
Compiled from "ClassCreateProcessTest.java"
public class io.github.binglau.jvm.ClassCreateProcessTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #14.#34 // java/lang/Object."<init>":()V
#2 = Class #35 // io/github/binglau/jvm/C
#3 = Methodref #2.#34 // io/github/binglau/jvm/C."<init>":()V
#4 = Fieldref #5.#36 // io/github/binglau/jvm/ClassCreateProcessTest.c:Lio/github/binglau/jvm/C;
#5 = Class #37 // io/github/binglau/jvm/ClassCreateProcessTest
#6 = Methodref #5.#34 // io/github/binglau/jvm/ClassCreateProcessTest."<init>":()V
#7 = Fieldref #38.#39 // java/lang/System.out:Ljava/io/PrintStream;
#8 = Methodref #40.#41 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#9 = Class #42 // io/github/binglau/jvm/B
#10 = Methodref #9.#34 // io/github/binglau/jvm/B."<init>":()V
#11 = Fieldref #5.#43 // io/github/binglau/jvm/ClassCreateProcessTest.b:Lio/github/binglau/jvm/B;
#12 = String #44 // A created
#13 = Methodref #40.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#14 = Class #46 // java/lang/Object
#15 = Utf8 b
#16 = Utf8 Lio/github/binglau/jvm/B;
#17 = Utf8 c
#18 = Utf8 Lio/github/binglau/jvm/C;
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 LocalVariableTable
#24 = Utf8 this
#25 = Utf8 Lio/github/binglau/jvm/ClassCreateProcessTest;
#26 = Utf8 main
#27 = Utf8 ([Ljava/lang/String;)V
#28 = Utf8 args
#29 = Utf8 [Ljava/lang/String;
#30 = Utf8 a
#31 = Utf8 <clinit>
#32 = Utf8 SourceFile
#33 = Utf8 ClassCreateProcessTest.java
#34 = NameAndType #19:#20 // "<init>":()V
#35 = Utf8 io/github/binglau/jvm/C
#36 = NameAndType #17:#18 // c:Lio/github/binglau/jvm/C;
#37 = Utf8 io/github/binglau/jvm/ClassCreateProcessTest
#38 = Class #47 // java/lang/System
#39 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#40 = Class #50 // java/io/PrintStream
#41 = NameAndType #51:#52 // println:(Ljava/lang/Object;)V
#42 = Utf8 io/github/binglau/jvm/B
#43 = NameAndType #15:#16 // b:Lio/github/binglau/jvm/B;
#44 = Utf8 A created
#45 = NameAndType #51:#53 // println:(Ljava/lang/String;)V
#46 = Utf8 java/lang/Object
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 java/io/PrintStream
#51 = Utf8 println
#52 = Utf8 (Ljava/lang/Object;)V
#53 = Utf8 (Ljava/lang/String;)V
{
public io.github.binglau.jvm.ClassCreateProcessTest(); // 在实例创建出来的时候调用,包括调用new操作符;调用Class或Java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;通过java.io.ObjectInputStream类的getObject()方法反序列化。
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class io/github/binglau/jvm/C
8: dup
9: invokespecial #3 // Method io/github/binglau/jvm/C."<init>":()V
12: putfield #4 // Field c:Lio/github/binglau/jvm/C;
15: return
LineNumberTable:
line 15: 0
line 9: 4
line 15: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 this Lio/github/binglau/jvm/ClassCreateProcessTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class io/github/binglau/jvm/ClassCreateProcessTest
3: dup
4: invokespecial #6 // Method "<init>":()V
7: astore_1
8: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
15: return
LineNumberTable:
line 18: 0
line 19: 8
line 20: 15
LocalVariableTable:
Start Length Slot Name Signature
0 16 0 args [Ljava/lang/String;
8 8 1 a Lio/github/binglau/jvm/ClassCreateProcessTest;
static {}; // 在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #9 // class io/github/binglau/jvm/B
3: dup
4: invokespecial #10 // Method io/github/binglau/jvm/B."<init>":()V
7: putstatic #11 // Field b:Lio/github/binglau/jvm/B;
10: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
13: ldc #12 // String A created
15: invokevirtual #13 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
18: return
LineNumberTable:
line 8: 0
line 12: 10
line 13: 18
}
SourceFile: "ClassCreateProcessTest.java"
binglau@BingLaudeMBP ~/c/c/J/basic>
static的方法其实是<clinit>
方法,在 <init>
也就是实例化之前执行,所有 先 new B 然后这边调用 #7 并 ldc 也就是其中的 static {} 方法中的 sout 执行 打印出 String A created,此时还没有对 A 进行初始化,A 初始化时候创建了 C。
所以 B 是在 3 时候创建的,C 是在 5 创建的,A 是在 5之后创建完成的。
可能出现 StackOverflowError 处
虚拟机(本地方法)栈中出现:
- 线程请求的栈深度大于虚拟机所允许的深度
/**
* 虚拟机栈和本地方法栈溢出
* VM Args: -Xss128k
* @author Administrator
*
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable{
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length: " + oom.stackLength);
throw e;
}
}
}
可能出现 OutOfMemoryError 处
虚拟机(本地方法)栈中出现:
- 如果虚拟机栈可以动态扩展,扩展时无法申请到足够的内存(实验不出来,如果是不断创建线程可以,但是这与栈空间十分足够大不存在关系,而是与操作系统对进程分配内存限制有关。)
堆:
- 在堆中没有内存完成实例分配,并且堆也无法再扩展时
import java.util.ArrayList;
import java.util.List;
/**
* Java堆用于存储对象实例,我们只要不断创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,就会在对象数量到达最大堆的容量限制后产生内存溢出异常。
* VM Args: -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
* @author Administrator
*
*/
public class HeapOOM {
static class OOMObject{
private String name;
public OOMObject(String name) {
this.name = name;
}
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
long i = 1;
while(true) {
list.add(new OOMObject("IpConfig..." + i++));
}
}
}
方法区 & 运行时常量池
- 无法满足内存分配需求
import java.util.ArrayList;
import java.util.List;
/**
* 方法区、运行时常量池溢出
* 方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述符、方法描述等。对于这个区域的测试,
* 基本的思路是运行时产生大量的类去填满方法区,直到溢出。比如动态代理会生成动态类。
* 运行时常量池分配在方法区内,可以通过 -XX:PermSize和 -XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。
* VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
* @author Administrator
*
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
直接内存:
- 本机内存限制,这个调整
-Xmx
也没用,直接内存动态扩展时出现
/**
* DirectMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆的最大值-Xmx指定一样。
* VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
* @author Administrator
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true) {
unsafe.allocateMemory(_1MB);
}
}
}
OOM 时 dump 日志
-XX:+HeapDumpOnOutOfMemoryError
参考书目
《深入理解 Java 虚拟机》
IO模式学习
问题引入
这是我们通常所写的程序
def normal():
for i in range(10):
print(i)
if __name__ == '__main__':
print('begin')
normal()
print('end')
#result
begin
0
1
2
3
4
5
6
7
8
9
end
通常我们的输入需要等到上条输入结束之后才能进行,这也许是我们通常最想要得到的结果,但是有某些时候如果我们所需要的结果没有前后文影响的情况下,我们可能更希望它不是这么死板地来执行的,而是异步执行,最典型的一个例子就是访问网站,网站在进行渲染的时候用到了大量的IO操作,而其他用户不可能等到一个用户渲染完之后在进行渲染,否则会造成比较差的用户体验。
事实上,所有的IO操作(如数据库查询,读写文件等)都回造成阻塞,它们都会让我们无法利用到IO执行这一期间的计算机资源。
为了解决这个问题,计算机引入了一些IO模式的区别。
Linux下常见IO模式介绍
首先是同步IO与异步IO介绍:
同步I/O操作:实际的I/O操作将导致请求进程阻塞,直到I/O操作完成。
异步I/O操作(AIO):实际的I/O操作不导致请求进程阻塞。
接下来是Linux中常见的IO模式介绍:
- **阻塞式I/O:**应用进程调用I/O操作时阻塞,只有等待要操作的数据准备好,并复制到应用进程的缓冲区中才返回。
- **非阻塞式I/O:**当应用进程要调用的I/O操作会导致该进程进入阻塞状态时,该I/O调用返回一个错误,一般情况下,应用进程需要利用轮询的方式来检测某个操作是否就绪。数据就绪后,实际的I/O操作会等待数据复制到应用进程的缓冲区中以后才返回。
- **I/O多路复用:**阻塞发生在select/poll的系统调用上,而不是阻塞在实际的I/O系统调用上。select/poll发现有数据就绪后,通过实际的I/O操作将数据复制到应用进程的缓冲区中。
- **异步I/O:**应用进程通知内核开始一个异步I/O操作,并让内核在整个操作(包含将数据从内核复制到应该进程的缓冲区)完成后通知应用进程。
从上面图中可以看出,该图把I/O操作分为两个阶段,第一阶段等待数据可用,第二阶段将数据从内核复制到用户空间。前三种模型的区别在于第一阶段(阻塞式I/O阻塞在I/O操作上,非阻塞式I/O轮询,I/O复用阻塞在select/poll或者epoll上),第二阶段都是一样的,即这里的阻塞不阻塞体现在第一阶段,从这方面来说I/O复用类型也可以归类到阻塞式I/O,它与阻塞式I/O的区别在于阻塞的系统调用不同。而异步I/O的两个阶段都不会阻塞进程。
可以看出,I/O模式中1,2,3都属于同步IO的操作,因为其在等待数据的时候都有I/O操作完成之前都会被阻塞。而只有4属于异步IO操作(AIO)。
在Linux下有两种称为AIO的的接口。一个是由glibc提供,是由多线程来模拟:数据等待和数据复制的工作,由glibc创建线程来完成。数据复制完成后,执行I/O操作的线程通过回调函数的方式通知应用线程(严格来讲,这种方式不能算真正的AIO,因为用来执行实际I/O操作的线程还是阻塞在I/O操作上,只不过从应用进程的角度来看是异步方式的)。另一种是由内核提供的Kernel AIO,可以做到真正的内核异步通知(这种方式对读写方式,写入大小及偏移都有严格的要求),并且不支持网络I/O,其实现原理本质上与下面要介绍的IOCP类似。
还有一种称为IOCP(Input/Output Completion Port)的AIO。从实现原理上讲,IOCP做完I/O操作后,将结果封装成完成包(completion packet)入队到完成端口的队列(FIFO)中去,应用线程从队列中读取到完成消息后,处理后续逻辑。从这方面来讲,IOCP类似生产者-消费者模型:生产者为内核,收到应用线程的I/O请求后,等待数据可用,并将结果数据复制到应用线程指定的缓冲区中后,然后入队一个完成消息;消费者为应用线程,一开始向内核提交I/O请求,并在队列上等待内核的完成消息(只不过,IOCP对同时可运行的消费者有限制),收到完成消息后,进行后续处理。
Python的实现
Python非阻塞IO
介绍Select
模块:该模块可以访问大多数操作系统中的select()
和poll()
函数, Linux2.5+支持的epoll()和大多数BSD支持的kqueue()。
select()
方法:该方法监听多个文件描述符的数组,当其返回的时候,系统内核就会将数组中已经就绪的文件描述符修改其标志位,使得进程可以获得这些文件描述符并进行相应的操作。注意,改方法在单个进程内监听的文件描述符数量存在限制,在Linux下一般是1024。
poll()
方法:与select()
几乎一样,但是不存在数量上的限制。
poll
和select
同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。另外,select()
和poll()
将就绪的文件描述符告诉进程后,如果进程没有对其进行IO操作,那么下次调用select()
和poll()
的时候 将再次报告这些文件描述符,所以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。不过poll
并不适用于windows平台。
Python中的select
模块以列表形式接受四个参数,分别是需要监控的可读文件对象,可写文件对象,产生异常的文件对象和超时设置(可省略),当监控的对象发生变化时,select会返回发生变化的对象列表。
以下是select.select()
的官方文档:
select.select(rlist, wlist, xlist[, timeout])
This is a straightforward interface to the Unix select() system call. The first three arguments are sequences of ‘waitable objects’: either integers representing file descriptors or objects with a parameterless method named fileno() returning such an integer:
`rlist`: wait until ready for reading `wlist`: wait until ready for writing `xlist`: wait for an “exceptional condition” (see the manual page for what your system considers such a condition)
Empty sequences are allowed, but acceptance of three empty sequences is platform-dependent. (It is known to work on Unix but not on Windows.) The optional timeout argument specifies a time-out as a floating point number in seconds. When the timeout argument is omitted the function blocks until at least one file descriptor is ready. A time-out value of zero specifies a poll and never blocks.
The return value is a triple of lists of objects that are ready: subsets of the first three arguments. When the time-out is reached without a file descriptor becoming ready, three empty lists are returned.
Among the acceptable object types in the sequences are Python file objects (e.g. sys.stdin, or objects returned by open() or os.popen()), socket objects returned by socket.socket(). You may also define a wrapper class yourself, as long as it has an appropriate fileno() method (that really returns a file descriptor, not just a random integer).
我们此处使用网上得到的一个聊天室来讲解:
#!/usr/bin/env python
#encoding:utf-8
import socket
import select
import sys
import signal
class ChatServer():
def __init__(self,host,port,timeout=10,backlog=5):
#记录连接的客户端数量
self.clients = 0
#存储连接的客户端socket和地址对应的字典
self.clientmap = {}
#存储连接的客户端socket
self.outputs = []
#建立socket
self.server=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((host, port))
self.server.listen(backlog)
#增加信号处理
signal.signal(signal.SIGINT, self.sighandler)
def sighandler(self):
sys.stdout.write("Shutdown Server......\n")
#向已经连接客户端发送关系信息,并主动关闭socket
for output in self.outputs:
output.send("Shutdown Server")
output.close()
#关闭listen
self.server.close()
sys.stdout.flush()
#主函数,用来启动服务器
def run(self):
#需要监听的可读对象
inputs = [self.server]
runing = True
#添加监听主循环
while runing:
try:
#此处会被select模块阻塞,只有当监听的三个参数发生变化时,select才会返回
readable, writeable, exceptional = select.select(inputs, self.outputs, [])
except select.error,e:
break
#当返回的readable中含有本地socket的信息时,表示有客户端正在请求连接
if self.server in readable:
#接受客户端连接请求
client,addr=self.server.accept()
sys.stdout.write("New Connection from %s\n"%str(addr))
sys.stdout.flush()
#更新服务器上客户端连接情况
#1,数量加1
#2,self.outputs增加一列
#3,self.clientmap增加一对
#4, 给input添加可读监控
self.clients += 1
#添加写入对象
self.outputs.append(client)
self.clientmap[client] = addr
inputs.append(client)
#readable中含有已经添加的客户端socket,并且可读
#说明 1,客户端有数据发送过来或者 2,客户端请求关闭
elif len(readable) != 0:
#1, 取出这个列表中的socket
csock = readable[0]
#2, 根据这个socket,在事先存放的clientmap中,去除客户端的地址,端口的详细信息
host,port = self.clientmap[csock]
#3,取数据, 或接受关闭请求,并处理
#注意,这个操作是阻塞的,但是由于数据是在本地缓存之后,所以速度会非常快
try:
data = csock.recv(1024).strip()
for cs in self.outputs:
if cs != csock:
cs.send("%s\n"%data)
except socket.error,e:
self.clients -= 1
inputs.remove(csock)
self.outputs.remove(csock)
del self.clientmap[csock]
#print self.outputs
self.server.close()
if __name__ == "__main__":
chat=ChatServer("",8008)
chat.run()
可以看出这里select.select()
所选择的对象均是通道, 此时多个客户端可以同时进行通话,而不需要等待其他客户端。这里实验客户端可以使用telnet
来进行操作
Python的异步IO
Python 3.4标准库有一个新模块asyncio
,用来支持异步I/O。
asyncio
的编程模型就是一个消息循环。我们从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
实例:
import threading
import asyncio
@asyncio.coroutine
def hello():
print('Hello world! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
#Result:
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
Hello world! (<_MainThread(MainThread, started 140735195337472)>)
(暂停约1秒)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)
Hello again! (<_MainThread(MainThread, started 140735195337472)>)
@asyncio.coroutine
把一个generator
标记为coroutine
类型,然后,我们就把这个coroutine
扔到EventLoop
中执行。
yield from
语法可以让我们方便地调用另一个generator
。由于asyncio.sleep()
也是一个coroutine
,所以线程不会等待asyncio.sleep()
,而是直接中断并执行下一个消息循环。当asyncio.sleep()
返回时,线程就可以从yield from
拿到返回值(此处是None
),然后接着执行下一行语句。
把asyncio.sleep(1)
看成是一个耗时1秒的IO操作,在此期间,主线程并未等待,而是去执行EventLoop
中其他可以执行的coroutine
了,因此可以实现并发执行。
由打印的当前线程名称可以看出,两个coroutine
是由同一个线程并发执行的。
如果把asyncio.sleep()
换成真正的IO操作,则多个coroutine
就可以由一个线程并发执行。
参考文档:
Python内存管理机制——内存模型
Python 中所有的内存管理机制都有两套实现,这两套实现由编译符号PYMALLOC_DEBUG
控制,当该符号被定义时,使用的是 debug 模式下的内存管理机制,这套机制在正常的内存管理动作之外,还会记录许多关于内存的信息,以方便 Python 在开发时进行调试;而当该符号未被定义时,Python 的内存管理机制只进行正常的内存管理动作。
// /include/Python.h
#if defined(Py_DEBUG) && defined(WITH_PYMALLOC) && !defined(PYMALLOC_DEBUG)
#define PYMALLOC_DEBUG
#endif
内存管理架构
在 Python 中,内存管理机制被抽象成下图这样的层次似结果。
Layer 0:
操作系统提供的内存管理接口,比如 C 运行时所提供的 malloc 和 free 接口。这层由操作系统实现并管理,Python 不能干涉这一层的行为。
Layer 1:
Python 基于第 0 层操作系统的内存管理接口包装而成的,这一层并没有在第 0 层加入太多的冻灾,其目的仅仅是为 Python 提供一层同意的 raw memory 的管理接口。防止操作系统的差异。第一次实现就是一组以PyMem_
为前缀的函数族。
// Include/pymem.h
PyAPI_FUNC(void *) PyMem_Malloc(size_t size);
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000
PyAPI_FUNC(void *) PyMem_Calloc(size_t nelem, size_t elsize);
#endif
PyAPI_FUNC(void *) PyMem_Realloc(void *ptr, size_t new_size);
PyAPI_FUNC(void) PyMem_Free(void *ptr);
// Object/obmalloc.c
// 这里使用了一个数据结构 PyMemAllocatorEx 里面定义了上下文及四种方法,上面有用 #ifdef...#endif 来初始化该数据结构方法
// 原始 C 语言方法也在该文件中
void *
PyMem_Malloc(size_t size)
{
/* see PyMem_RawMalloc() */
if (size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyMem.malloc(_PyMem.ctx, size);
}
void *
PyMem_Calloc(size_t nelem, size_t elsize)
{
/* see PyMem_RawMalloc() */
if (elsize != 0 && nelem > (size_t)PY_SSIZE_T_MAX / elsize)
return NULL;
return _PyMem.calloc(_PyMem.ctx, nelem, elsize);
}
void *
PyMem_Realloc(void *ptr, size_t new_size)
{
/* see PyMem_RawMalloc() */
if (new_size > (size_t)PY_SSIZE_T_MAX)
return NULL;
return _PyMem.realloc(_PyMem.ctx, ptr, new_size);
}
void
PyMem_Free(void *ptr)
{
_PyMem.free(_PyMem.ctx, ptr);
}
在第一层中,Python 还提供了面向 Python 中类型的内存分配器
// Include/pymem.h
/*
* Type-oriented memory interface
* ==============================
*
* Allocate memory for n objects of the given type. Returns a new pointer
* or NULL if the request was too large or memory allocation failed. Use
* these macros rather than doing the multiplication yourself so that proper
* overflow checking is always done.
*/
#define PyMem_New(type, n) \
( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
( (type *) PyMem_Malloc((n) * sizeof(type)) ) )
#define PyMem_NEW(type, n) \
( ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
( (type *) PyMem_MALLOC((n) * sizeof(type)) ) )
/*
* The value of (p) is always clobbered by this macro regardless of success.
* The caller MUST check if (p) is NULL afterwards and deal with the memory
* error if so. This means the original value of (p) MUST be saved for the
* caller's memory error handler to not lose track of it.
*/
#define PyMem_Resize(p, type, n) \
( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
(type *) PyMem_Realloc((p), (n) * sizeof(type)) )
#define PyMem_RESIZE(p, type, n) \
( (p) = ((size_t)(n) > PY_SSIZE_T_MAX / sizeof(type)) ? NULL : \
(type *) PyMem_REALLOC((p), (n) * sizeof(type)) )
在 PyMem_New
中,只要提供类型和数量,Python 会自动侦测其所需的内存空间大小。
Layer 2:
以 PyObje_为前缀的函数族,主要提供了创建 Python 对象的接口。这一套函数族又被唤作 Pymalloc 机制。
在第二层内存管理机制之上,对于 Python 中的一些常用对象,比如整数对象、字符串对象等,Python 又构建了更高抽象层次的内存管理策略。
真正在 Python 中发挥巨大作用、同时也是 GC 的藏身之处的内存管理机制所在层次。
Layer 3:
第三层的内存管理策略,主要就是对象缓冲池机制。(参加书中第一部分,或者下篇博客)。
小块空间的内存池
在 Python 中,许多时候申请的内存都是小块的内存,这些小块内存在申请后,很快又会被释放,由于这些内存的申请并不是为了创建对象,所以并没有对象一级的内存池机制。这就意味着 Python 在运行期间会大量地执行 malloc 和 free 操作,导致操作系统频繁地在用户态和核心态之间进行切换,这将严重影响 Python 的执行效率。为了提高 Python 的执行效率,Python 引入了一个内存池机制,用于管理对小块内存的申请和释放。这也就是之前提到的 Pymalloc 机制。
在 Python 中,整个小块内存池可以视为一个层次结构,在这个层次结构中,一共分为4层,从下至上分别是:block、pool、arena 和内存池。需要说明的是,block、pool 和 arena 都是 Python 代码中可以找到的实体,而最顶层的『内存池』只是一个概念上的东西,表示 Python 对于整个小块内存分配和释放行为的内存管理机制。
Block
在最底层,block 是一个确定大小的内存块。在 Python 中,有多种 block,不同种类的 block 都有不同的内存大小,这个内存大小的值被称为 size class。为了在当前主流的平台都能获得最佳性能,所有的 block 的长度都是8字节对齐的。
// Object/obmalloc.c
#define ALIGNMENT 8 /* must be 2^N */
#define ALIGNMENT_SHIFT 3
block 上限,当申请的内存大小小于这个上限时,Python 可以使用不同种类的 block 来满足对内存的需求;当神奇的内存大小超过这个上限,Python 就会对内存的请求转交给第一层的内存管理机制,即 PyMem 函数族,来处理。
// Object/obmalloc.c
#define SMALL_REQUEST_THRESHOLD 512
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
现在,需要指出一个相当关键的点,虽然我们这里谈论了很多block,但是在Python中,block只是一个概念,在Python源码中没有与之对应的实体存在。之前我们说对象,对象在Python源码中有对应的PyObject;我们说列表,列表在Python源码中对应PyListObject、PyType_List。这里的block就很奇怪了,它仅仅是概念上的东西,我们知道它是具有一定大小的内存,但它不与Python源码里的某个东西对应。然而,Python却提供了一个管理block的东西,这就是下面要剖析的pool。
Pool
一组 block 的集合称为一个 pool,换句话说,一个 pool 管理着一堆有固定大小的内存块。事实上,pool 管理着一大块内存,它由一定的策略,将这块大的内存划分为多个小的内存块。在 Python 中,一个 pool 的大小通常为一个系统内存页,由于当前大多数 Python 支持的系统的内存页都是4KB,所以 Python 内部也将一个 pool 的大小定义为4KB。
// Object/obmalloc.c
#define SYSTEM_PAGE_SIZE (4 * 1024)
#define SYSTEM_PAGE_SIZE_MASK (SYSTEM_PAGE_SIZE - 1)
/*
* Size of the pools used for small blocks. Should be a power of 2,
* between 1K and SYSTEM_PAGE_SIZE, that is: 1k, 2k, 4k.
*/
#define POOL_SIZE SYSTEM_PAGE_SIZE /* must be 2^N */
#define POOL_SIZE_MASK SYSTEM_PAGE_SIZE_MASK
// Object/obmalloc.c
/* When you say memory, my mind reasons in terms of (pointers to) blocks */
typedef uint8_t block;
/* Pool for small blocks. */
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};
4KB 中除去 pool_header,还有一大块内存就是 pool 中维护的 block 的集合占用的内存。
前面提到block 是有固定大小的内存块,因此,pool 也携带了大量这样的信息。一个 pool 管理的所有 block,它们的大小都是一样的。也就是说,一个 pool 可能管理了100个32个字节的 block,也可能管理了100个64个字节的 block,但是绝不会有一个管理了50个32字节的 block 和50个64字节的 block 的 pool 存在。每一个 pool 都和一个 size 联系在一起,更确切地说,都和一个 size class index 联系在一起。这就是 pool_header
中的 szindex 的意义。
假设我们手上现在有一块 4KB 的内存,来看看 Python 是如何将这块内存改造为一个管理32字节 block 的 pool,并从 pool 中取出第一块 block 的。
// [obmalloc.c]-[convert 4k raw memory to pool]
#define ROUNDUP(x) (((x) + ALIGNMENT_MASK) & ~ALIGNMENT_MASK)
#define POOL_OVERHEAD ROUNDUP(sizeof(struct pool_header))
#define struct pool_header* poolp
#define uchar block
poolp pool;
block* bp;
…… // pool指向了一块4kB的内存
pool->ref.count = 1;
//设置pool的size class index
pool->szidx = size;
//将size class index转换为size,比如3转换为32字节
size = INDEX2SIZE(size);
//跳过用于pool_header的内存,并进行对齐
bp = (block *)pool + POOL_OVERHEAD;
//实际就是pool->nextoffset = POOL_OVERHEAD+size+size
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
// bp 返回的实际是一个地址,这个地址之后有将近 4KB 的内存实际上都是可用的,但是可用肯定申请内存的函数只会使用[bp, bp+size] 这个区间的内存,这是由 size class index 可用保证的。
return (void *)bp;
注意其中的实线箭头是指针,但是虚线箭头不是代表指针,是偏移位置的形象表示。在nextoffset和maxnextoffset中存储的是相对于poo头部的偏移位置。
// [obmalloc.c]-[allocate block]
if (pool != pool->nextpool) {
++pool->ref.count;
bp = pool->freeblock;
……
if (pool->nextoffset <= pool->maxnextoffset) {
//有足够的block空间
pool->freeblock = (block *)pool + pool->nextoffset;
pool->nextoffset += INDEX2SIZE(size);
// 设置*freeblock 的动作正是建立离散自由 block 链表的关键所在
*(block **)(pool->freeblock) = NULL;
return (void *)bp;
}
}
原来freeblock指向的是下一个可用的block的起始地址,这一点在上图中也可以看得出。当再次申请32字节的block时,只需返回freeblock指向的地址就可以了,很显然,这时freeblock需要向前进,指向下一个可用的block。这时,nextoffset现身了。
在pool header中,nextoffset和maxoffset是两个用于对pool中的block集合进行迭代的变量:从初始化pool的结果及图16-2中可以看到,它所指示的偏移位置正好指向了freeblock之后的下一个可用的block的地址。从这里分配block的动作也可以看到,在分配了block之后,freeblock和nextoffset都会向前移动一个block的距离,如此反复,就可对所有的block进行一次遍历。而maxnextoffset指名了该pool中最后一个可用的block距pool开始位置的便移,它界定了pool的边界,当nextoffset > maxnextoff 时,也就意味着已经遍历完了pool中所有的block了。
可以想像,一旦Python运转起来,内存的释放动作将会导致pool中出现大量的离散的自由block,Python必须建立一种机制,将这些离散的自由block组织起来,再次使用。这个机制就是所谓的自由block链表。这个链表的关键就着落在pool_header中的那个freeblock身上。
// [obmalloc.c]
//基于地址P获得离P最近的pool的边界地址
#define POOL_ADDR(P) ((poolp)((uptr)(P) & ~(uptr)POOL_SIZE_MASK))
void PyObject_Free(void *p)
{
poolp pool;
block *lastfree;
poolp next, prev;
uint size;
pool = POOL_ADDR(p);
//判断p指向的block是否属于pool
if (Py_ADDRESS_IN_RANGE(p, pool)) {
// 被释放的第一个字节的值被设置为当前的 freeblock 的值
*(block **)p = lastfree = pool->freeblock;
// pool 的值被更新,指向其首地址,则一个 block 被插入到了离散自由的 block 链表中
pool->freeblock = (block *)p;
……
}
}
arena
在 Python 中,多个 pool 聚合的结果就是一个 arena。
// Object/obmalloc.c
// arena 大小的默认值
#define ARENA_SIZE (256 << 10) /* 256KB */
//arena_object 是 arena 的一部分
typedef struct pool_header *poolp;
/* Record keeping for arenas. */
struct arena_object {
/* The address of the arena, as returned by malloc. Note that 0
* will never be returned by a successful malloc, and is used
* here to mark an arena_object that doesn't correspond to an
* allocated arena.
*/
uintptr_t address;
/* Pool-aligned pointer to the next pool to be carved off. */
block* pool_address;
/* The number of available pools in the arena: free pools + never-
* allocated pools.
*/
uint nfreepools;
/* The total number of pools in the arena, whether or not available. */
uint ntotalpools;
/* Singly-linked list of available pools. */
struct pool_header* freepools;
/* Whenever this arena_object is not associated with an allocated
* arena, the nextarena member is used to link all unassociated
* arena_objects in the singly-linked `unused_arena_objects` list.
* The prevarena member is unused in this case.
*
* When this arena_object is associated with an allocated arena
* with at least one available pool, both members are used in the
* doubly-linked `usable_arenas` list, which is maintained in
* increasing order of `nfreepools` values.
*
* Else this arena_object is associated with an allocated arena
* all of whose pools are in use. `nextarena` and `prevarena`
* are both meaningless in this case.
*/
struct arena_object* nextarena;
struct arena_object* prevarena;
};
『未使用』的 arena 和『可用』的 arena
实际上,在Python中,确实会存在多个arena_object构成的集合,但是这个集合并不构成链表,而是构成了一个arena的数组。数组的首地址由arenas维护,这个数组就是Python中的通用小块内存的内存池;另一方面,nextarea和prevarea也确实是用来连接arena_object组成链表的。
pool_header管理的内存与pool_header自身是一块连续的内存,而areana_object与其管理的内存则是分离的。这后面隐藏着这样一个事实:当pool_header被申请时,它所管理的block集合的内存一定也被申请了;但是当aerna_object被申请时,它所管理的pool集合的内存则没有被申请。换句话说,arena_object和pool集合在某一时刻需要建立联系。注意,这个建立联系的时刻是一个关键的时刻,Python从这个时刻一刀切下,将一个arena_object切分为两种状态。
当一个arena的area_object没有与pool集合建立联系时,这时的arena处于“未使用”状态;一旦建立了联系,这时arena就转换到了“可用”状态。对于每一种状态,都有一个arena的链表。“未使用”的arena的链表表头是unused_arena_objects、arena与arena之间通过nextarena连接,是一个单向链表;而“可用”的arena的链表表头是usable_arenas、arena与arena之间通过nextarena和prevarena连接,是一个双向链表。
申请 arena
// [obmalloc.c]
//arenas管理着arena_object的集合
static struct arena_object* arenas = NULL;
//当前arenas中管理的arena_object的个数
static uint maxarenas = 0;
//“未使用的”arena_object链表
static struct arena_object* unused_arena_objects = NULL;
//“可用的”arena_object链表
static struct arena_object* usable_arenas = NULL;
//初始化时需要申请的arena_object的个数
#define INITIAL_ARENA_OBJECTS 16
static struct arena_object* new_arena(void)
{
struct arena_object* arenaobj;
uint excess; /* number of bytes above pool alignment */
//[1]: 判断是否需要扩充“未使用的”arena_object列表
if (unused_arena_objects == NULL) { // if one
uint i;
uint numarenas;
size_t nbytes;
//[2]: 确定本次需要申请的arena_object的个数,并申请内存
numarenas = maxarenas ? maxarenas << 1 : INITIAL_ARENA_OBJECTS;
if (numarenas <= maxarenas)
return NULL; //overflow(溢出)
nbytes = numarenas * sizeof(*arenas);
if (nbytes / sizeof(*arenas) != numarenas)
return NULL; //overflow
arenaobj = (struct arena_object *)realloc(arenas, nbytes);
if (arenaobj == NULL)
return NULL;
arenas = arenaobj;
//[3]: 初始化新申请的arena_object,并将其放入unused_arena_objects链表中
for (i = maxarenas; i < numarenas; ++i) {
arenas[i].address = 0; /* mark as unassociated */
arenas[i].nextarena = i < numarenas - 1 ? &arenas[i+1] : NULL;
}
/* Update globals. */
unused_arena_objects = &arenas[maxarenas];
maxarenas = numarenas;
} // end one
//[4]: 从unused_arena_objects链表中取出一个“未使用的”arena_object
arenaobj = unused_arena_objects;
unused_arena_objects = arenaobj->nextarena;
assert(arenaobj->address == 0);
//[5]: 申请arena_object管理的内存
arenaobj->address = (uptr)malloc(ARENA_SIZE);
++narenas_currently_allocated;
//[6]: 设置pool集合的相关信息
arenaobj->freepools = NULL;
arenaobj->pool_address = (block*)arenaobj->address;
arenaobj->nfreepools = ARENA_SIZE / POOL_SIZE;
//将pool的起始地址调整为系统页的边界
excess = (uint)(arenaobj->address & POOL_SIZE_MASK);
if (excess != 0) {
--arenaobj->nfreepools;
arenaobj->pool_address += POOL_SIZE - excess;
}
arenaobj->ntotalpools = arenaobj->nfreepools;
return arenaobj;
}
内存池
可用 pool 缓冲池——usedpools
Python 内部默认的小块内存与大块内存的分界点定在512个字节,这个分界点由前面我们看到的名为SMALL_REQUEST_THRESHOLD
的符号控制。也就是说,当申请的内存小于512字节时,PyObject_Malloc
会在内存池中申请内存;当申请的内存大于512字节时,PyObject_Malloc
的行为将退化为 malloc 的行为。
在Python中,pool是一个有size概念的内存管理抽象体,一个pool中的block总是有确定的大小,这个pool总是和某个size class index对应,还记得pool_head中的那个szidx么?而arena是没有size概念的内存管理抽象体,这就意味着,同一个arena,在某个时刻,其内的pool集合可能都是管理的32字节的block;而到了另一时刻,由于系统需要,这个arena可能被重新划分,其中的pool集合可能改为管理64字节的block了,甚至pool集合中一半管理32字节,一半管理64字节。这就决定了在进行内存分配和销毁时,所有的动作都是在pool上完成的。
内存池中的pool,不仅是一个有size概念的内存管理抽象体,而且,更进一步的,它还是一个有状态的内存管理抽象体。一个pool在Python运行的任何一个时刻,总是处于以下三种状态的一种:
- used状态:pool中至少有一个block已经被使用,并且至少有一个block还未被使用。这种状态的pool受控于Python内部维护的usedpools数组;
- full状态:pool中所有的block都已经被使用,这种状态的pool在arena中,但不在arena的freepools链表中;
- empty状态:pool中所有的block都未被使用,处于这个状态的pool的集合通过其pool_header中的nextpool构成一个链表,这个链表的表头就是arena_object中的freepools;
Python内部维护的usedpools数组是一个非常巧妙的实现,维护着所有的处于used状态的pool。当申请内存时,Python就会通过usedpools寻找到一块可用的(处于used状态的)pool,从中分配一个block。一定有一个与usedpools相关联的机制,完成从申请的内存的大小到size class index之间的转换,否则Python也就无法寻找到最合适的pool了。这种机制与usedpools的结构有密切的关系,我们来看一看usedpools的结构。
一定有一个与usedpools相关联的机制,完成从申请的内存的大小到size class index之间的转换,否则Python也就无法寻找到最合适的pool了。这种机制与usedpools的结构有密切的关系,我们来看一看usedpools的结构。
// [obmalloc.c]
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
typedef uchar block;
#define PTA(x) ((poolp )((uchar *)&(usedpools[2*(x)]) - 2*sizeof(block *)))
#define PT(x) PTA(x), PTA(x)
// 这里的 poolp 指的就是 pool_head
static poolp usedpools[2 * ((NB_SMALL_SIZE_CLASSES + 7) / 8) * 8] = {
PT(0), PT(1), PT(2), PT(3), PT(4), PT(5), PT(6), PT(7)
#if NB_SMALL_SIZE_CLASSES > 8 //指明了在当前的配置之下,一共有多少个 size class
, PT(8), PT(9), PT(10), PT(11), PT(12), PT(13), PT(14), PT(15)
……
#endif
}
其中的NB_SMALL_SIZE_CLASSES
指明了在当前的配置之下,一共有多少个size class。
// [obmalloc.c]
#define NB_SMALL_SIZE_CLASSES (SMALL_REQUEST_THRESHOLD / ALIGNMENT)
Python会首先获得 size class index,通过 size = (uint )(nbytes - 1) >> ALIGNMENT_SHIFT,得到 size class index 为3。在usedpools 中,寻找第3+3=6个元素,发现 usedpools[6] 的值是指向 usedpools[4] 的地址。有些迷惑了,对吧?好了,现在对照 pool_header 的定义来看一看 usedpools[6] -> nextpool 这个指针指向哪里了呢?是从 usedpools[6](即usedpools+4)开始向后偏移8个字节(一个ref的大小加上一个freeblock的大小)后的内存,不正是 usedpools[6] 的地址(即usedpools+6)吗?这是Python内部使用的一个 trick。
想象一下,当我们手中有一个size class为32字节的pool,想要将其放入这个usedpools中时,需要怎么做呢?从上面的描述可以看到,只需要进行usedpools[i+i]->nextpool = pool即可,其中i为size class index,对应于32字节,这个i为3。当下次需要访问size class为32字节(size class index为3)的pool时,只需要简单地访问usedpool[3+3]就可以得到了。Python正是使用这个usedpools快速地从众多的pool中快速地寻找到一个最适合当前内存需求的pool,从中分配一块block。
// [obmalloc.c]
void* PyObject_Malloc(size_t nbytes)
{
block *bp;
poolp pool;
poolp next;
uint size;
if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
LOCK();
//获得size class index
size = (uint )(nbytes - 1) >> ALIGNMENT_SHIFT;
pool = usedpools[size + size];
//usedpools中有可用的pool
if (pool != pool->nextpool) {
……//usedpools中有可用的pool
}
…… //usedpools中无可用pool,尝试获取empty状态pool
}
Pool 的初始化
当 Python 启动之后,在 usedpools 这个小块空间内存池中,并不存在任何可用的内存,准确地说,不存在任何可用的pool。在这里,Python 采用了延迟分配的策略,即当我们确实开始申请小块内存时,Python 才开始建立这个内存池。
考虑一下这样的情况,当申请32字节内存时,从“可用的” arena 中取出其中一个 pool 用作32字节的 pool 。当下一次内存分配请求分配64字节的内存时,Python 可以直接使用当前“可用的” arena 的另一个 pool 即可。这正如我们前面所说, arena 没有 size class 的属性,而 pool 才有(见下面代码)。
// [obmalloc.c]
void * PyObject_Malloc(size_t nbytes)
{
block *bp;
poolp pool;
poolp next;
uint size;
if ((nbytes - 1) < SMALL_REQUEST_THRESHOLD) {
LOCK();
size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
pool = usedpools[size + size];
if (pool != pool->nextpool) {
…… //usedpools中有可用的pool
}
//usedpools中无可用pool,尝试获取empty状态pool
//[1]: 如果usable_arenas链表为空,则创建链表
if (usable_arenas == NULL) {
//申请新的arena_object,并放入usable_arenas链表
usable_arenas = new_arena();
usable_arenas->nextarena = usable_arenas->prevarena = NULL;
}
//[2]: 从usable_arenas链表中第一个arena的freepools中抽取一个可用的pool
pool = usable_arenas->freepools;
if (pool != NULL) {
usable_arenas->freepools = pool->nextpool;
//[3]: 调整usable_arenas链表中第一个arena中的可用pool数量
//如果调整后数量为0,则将该arena从usable_arenas链表中摘除
--usable_arenas->nfreepools;
if (usable_arenas->nfreepools == 0) {
usable_arenas = usable_arenas->nextarena;
if (usable_arenas != NULL) {
usable_arenas->prevarena = NULL;
}
}
init pool:
……
}
初始化之一
好了,现在我们手里有了一块用于32字节内存分配的pool,为了以后提高内存分配的效率,我们需要将这个pool放入到usedpools中。这一步,叫做init pool。
// [obmalloc.c]
#define ROUNDUP(x) (((x) + ALIGNMENT_MASK) & ~ALIGNMENT_MASK)
#define POOL_OVERHEAD ROUNDUP(sizeof(struct pool_header))
void * PyObject_Malloc(size_t nbytes) {
……
init_pool:
//[1]: 将pool放入usedpools中
next = usedpools[size + size]; /* == prev */
pool->nextpool = next;
pool->prevpool = next;
next->nextpool = pool;
next->prevpool = pool;
pool->ref.count = 1;
//[2]:pool在之前就具有正确的size结构,直接返回pool中的一个block
if (pool->szidx == size) {
bp = pool->freeblock;
pool->freeblock = *(block **)bp;
UNLOCK();
return (void *)bp;
}
//[3]: 初始化pool header,将freeblock指向第二个block,返回第一个block
pool->szidx = size;
size = INDEX2SIZE(size);
bp = (block *)pool + POOL_OVERHEAD;
pool->nextoffset = POOL_OVERHEAD + (size << 1);
pool->maxnextoffset = POOL_SIZE - size;
pool->freeblock = bp + size;
*(block **)(pool->freeblock) = NULL;
UNLOCK();
return (void *)bp;
……
}
在什么样的情况下才会发生一个 pool 从 empty 状态转换为 used 状态呢?假设申请的内存的 size class index 为 i,且 usedpools[i+i] 处没有处于 used 状态的 pool ,同时在 Python 维护的全局变量 freepools 中还有处于 empty 的 pool ,那么位于 freepools 所维护的 pool 链表头部的 pool 将被取出来,放入 usedpools 中,并从其内部分配一块 block 。同时,这个 pool 也就从 empty 状态转换到了 used 状态。下面我们看一看这个行为在代码中是如何体现的。
// [obmalloc.c]
……
pool = usable_arenas->freepools;
if (pool != NULL) {
usable_arenas->freepools = pool->nextpool;
…… //调整usable_arenas->nfreepools和usable_arenas自身
[init_pool]
}
初始化之二
我们现在可以来看看,当PyObject_Malloc从new_arena中得到一个新的arena后,是怎么样来初始化其中的pool集合,并最终完成PyObject_Malloc函数的分配一个block这个终极任务的。
// [obmalloc.c]
#define DUMMY_SIZE_IDX 0xffff /* size class of newly cached pools */
void * PyObject_Malloc(size_t nbytes)
{
block *bp;
poolp pool;
poolp next;
uint size;
……
//[1]:从arena中取出一个新的pool
pool = (poolp)usable_arenas->pool_address;
// 设置 pool 中的 arenaindex,这个 index 实际上就是 pool 所在的 arena 位于 arenas 所指的数组的序号。用于判断一个 block 是否在某个 pool 中。
pool->arenaindex = usable_arenas - arenas;
// 随后 Python 将新得到的 pool 的 szidx 设置为 0xffff,表示从没管理过 block 集合。
pool->szidx = DUMMY_SIZE_IDX;
// 调整刚获得的 arena 中的 pools 集合,甚至可能调整 usable_arenas
usable_arenas->pool_address += POOL_SIZE;
--usable_arenas->nfreepools;
if (usable_arenas->nfreepools == 0) {
/* Unlink the arena: it is completely allocated. */
usable_arenas = usable_arenas->nextarena;
if (usable_arenas != NULL) {
usable_arenas->prevarena = NULL;
}
}
goto init_pool;
……
}
block 的释放
pool 的状态变更最为常见的还是之前是 used 之后也是 used:
在pool的状态保持used状态这种情况下,Python仅仅将block重新放入到自由block链表中,并调整了pool中的ref.count这个引用计数。
// [obmalloc.c]
void PyObject_Free(void *p)
{
poolp pool;
block *lastfree;
poolp next, prev;
uint size;
pool = POOL_ADDR(p);
if (Py_ADDRESS_IN_RANGE(p, pool)) {
//设置离散自由block链表
*(block **)p = lastfree = pool->freeblock;
pool->freeblock = (block *)p;
if (lastfree) { //lastfree有效,表明当前pool不是处于full状态
if (--pool->ref.count != 0) { //pool不需要转换为empty状态
return;
}
……
}
……
}
//待释放的内存在PyObject_Malloc中是通过malloc获得的
//所以要归还给系统
free(p);
}
当我们释放一个 block 后,可能会引起 pool 的状态的转变,这种转变可分为两种情况:
-
full状态转变为used状态
仅仅是将 pool 重新链回到 usedpools 中即可
[obmalloc.c] void PyObject_Free(void *p) { poolp pool; block *lastfree; poolp next, prev; uint size; pool = POOL_ADDR(p); if (Py_ADDRESS_IN_RANGE(p, pool)) { …… //当前pool处于full状态,在释放一块block后,需将其转换为used状态,并重新 //链入usedpools的头部 --pool->ref.count; size = pool->szidx; next = usedpools[size + size]; prev = next->prevpool; /* insert pool before next: prev <-> pool <-> next */ pool->nextpool = next; pool->prevpool = prev; next->prevpool = pool; prev->nextpool = pool; return; } …… }
-
used状态转变为empty状态
首先 Python 要做的是将empty状态的 pool 链入到 freepools 中去
// [obmalloc.c] void PyObject_Free(void *p) { poolp pool; block *lastfree; poolp next, prev; uint size; pool = POOL_ADDR(p); if (Py_ADDRESS_IN_RANGE(p, pool)) { *(block **)p = lastfree = pool->freeblock; pool->freeblock = (block *)p; if (lastfree) { struct arena_object* ao; uint nf; //ao->nfreepools if (--pool->ref.count != 0) { return; } // 将pool放入freepools维护的链表中 // 这里隐藏着一个类似于内存泄露的问题:arena 从来不释放 pool ao = &arenas[pool->arenaindex]; pool->nextpool = ao->freepools; ao->freepools = pool; nf = ++ao->nfreepools; …… } …… } …… }
现在开始处理 arena,分为四种情况:
-
如果arena中所有的pool都是empty的,释放pool集合占用的内存。
// [obmalloc.c] void PyObject_Free(void *p) { poolp pool; block *lastfree; poolp next, prev; uint size; pool = POOL_ADDR(p); struct arena_object* ao; uint nf; //ao->nfreepools …… //将pool放入freepools维护的链表中 ao = &arenas[pool->arenaindex]; pool->nextpool = ao->freepools; ao->freepools = pool; nf = ++ao->nfreepools; if (nf == ao->ntotalpools) { //调整usable_arenas链表 if (ao->prevarena == NULL) { usable_arenas = ao->nextarena; } else { ao->prevarena->nextarena = ao->nextarena; } if (ao->nextarena != NULL) { ao->nextarena->prevarena = ao->prevarena; } //调整unused_arena_objects链表 ao->nextarena = unused_arena_objects; unused_arena_objects = ao; //释放内存 free((void *)ao->address); //设置address,将arena的状态转为“未使用” ao->address = 0; --narenas_currently_allocated; } …… }
-
如果之前arena中没有了empty的pool,那么在usable_arenas链表中就找不到该arena,由于现在arena中有了一个pool,所以需要将这个aerna链入到usable_arenas链表的表头。
-
若arena中的empty的pool个数为n,则从usable_arenas开始寻找arena可以插入的位置,将arena插入到usable_arenas。这个操作的原因是由于usable_arenas实际上是一个有序的链表,从表头开始往后,每一个arena中的empty的pool的个数,即nfreepools,都不能大于前面的arena,也不能小于前面的arena。保持这种有序性的原因是分配block时,是从usable_arenas的表头开始寻找可用的arena的,这样,就能保证如果一个arena的empty pool数量越多,它被使用的机会就越少。因此,它最终释放其维护的pool集合的内存的机会就越大,这样就能保证多余的内存会被归还给系统。
-
其他情况,不进行任何对arena的处理。
-
内存池全景
参考资料
《Python 源码解析》
并发的姿势
并发的姿势
并行还是并发?
并发:同一时间应对多件事情的能力
并行:同一时间动手做多件事情的能力
并发:两队列人享有一台咖啡机,每列都能获得咖啡
并行:一队列人享有独有的咖啡机,互不干扰
而获取咖啡从局部来看又是串行,每个人都需要等前面人使用完成后才能使用(并发中也可能需要不定等别队列的人来获取)
Java 的线程与锁模型
线程:Java 并发中的基本单元,可以将一个线程看做一个控制流,线程之间通过共享内存进行通信。
使用锁来达到在使用共享内存时,线程之间的使用形成互斥的目的。
简单的哲学家问题:
package io.github.binglau.concurrency;
import io.github.binglau.bean.Chopstick;
import java.util.Random;
/**
* 文件描述: 哲学家问题
*/
public class Philosopher implements Runnable {
private Chopstick left, right;
private Random random;
public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
random = new Random();
}
@Override
public void run() {
// 死锁:当所有人都持有一只筷子并等待另一个放开一只筷子
try {
while (true) {
Thread.sleep(random.nextInt(100) * 10); // 思考时间
// 获取对象锁
synchronized (left) { // 拿起筷子 1
synchronized (right) { // 拿起筷子 2
System.out.println(Thread.currentThread().getName() + "-ok");
Thread.sleep(random.nextInt(100) * 10); // 进餐时间
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Chopstick c1 = new Chopstick();
Chopstick c2 = new Chopstick();
Chopstick c3 = new Chopstick();
Chopstick c4 = new Chopstick();
new Thread(new Philosopher(c1, c2)).start();
new Thread(new Philosopher(c2, c3)).start();
new Thread(new Philosopher(c3, c4)).start();
new Thread(new Philosopher(c4, c1)).start();
}
}
线程与所模型带来的三个主要危害:
- 静态条件
- 死锁
- 内存可见性(Java 内存模型定义了何时一个线程对内存的修改对另一个线程可见,这样两个线程都需要进行同步的情况下,线程获得的一个值可能已经是一个失效的值)
优势:
- 普遍,更接近本质,能解决几乎所有粒度的所有问题
缺点:
- 难以维护
- 难以测试(共享内存决定的不确定性)
『天然』的并发,函数式编程
;用于延时执行某个函数
(defn make-heavy [f]
(fn [& args]
(Thread/sleep 1000)
(apply f args)))
(time (doall (map (make-heavy inc) [1 2 3 4 5])))
(time (doall (pmap (make-heavy inc) [1 2 3 4 5])))
;"Elapsed time: 5012.47372 msecs"
;"Elapsed time: 1012.144992 msecs"
;pmap并行地将make-heavy包装后的inc作用在集合的5个元素上,总耗时就接近于于单个调用的耗时,也就是一秒。
;pmap的并行,从实现上来说,是集合有多少个元素就使用多少个线程
容易推理,便于测试,消除并发与串行的区别
通信顺序进程
package concurrency
import "fmt"
/**
* 文件描述:
*/
// goroutine 使用时候的一些问题
// 1. 主 goroutine 结束后 子 goroutine 也会结束
// 2. 容易发生死锁,就是即使所有工作已经完成了但
// 主 goroutine 无法获得工作 goroutine 的 完成状态。
// 死锁的另一个情况就是,当两个不同的 goroutine (或者线程)
// 都锁定了受保护的资源而且同时尝试去获取对方资源的时候
//
// 解决办法:
// 1. 下面所示主 goroutine 在一个 done 通道上等待
// 2. 使用 sync.WaitGroup 来让每个工作 goroutine 报告自己
// 的完成状态。但是,使用 sync.WaitGroup 本身也会产生死锁,
// 特别是当所有工作 goroutine 都处于锁定状态的时候(等待接收
// 通道的数据)调用 sync.WaitGroup.Wait()
// 通道为并发运行的 goroutine 之间提供了一种无锁的通信方式(尽管
// 内部实现可能使用了锁,但我们无需关系)。当一个通道发生通信时,发
// 送通道和接收通道(包括它们对应的 goroutine)都处于同步状态
// Go 语言并不保证在通道里发送指针或者引用类型(如切片或映射)的安全性,
// 因为指针指向的内容或者所引用的值可能在对方接收到时已被发送方修改。
// 所有,当涉及到指针和引用时,我们必须保证这些值在任何时候只能被一
// 个 goroutine 访问得到,也就是说,对这些值的访问必须是串行进行的。
// 除非文档特别声明传递这个指针是安全的。
type Job struct {
Id int
Name string
}
// 使用并发最简单的方式就是用一个 goroutine 来准备工作
// 然后让另一个 goroutine 来执行处理,让主 goroutine 和
// 一些通道来安排一切事情
func Demo() {
jobList := make([]Job, 10) // 任务列表
jobs := make(chan Job)
done := make(chan bool, len(jobList))
go func() { // 创建 goroutine
for _, job := range jobList { // 遍历 jobList 然后将每个工作发送到 jobs 通道
jobs <- job // 通道没有缓冲,所以马上阻塞,等待别的 goroutine 接收
}
close(jobs) // 发完任务之后关闭通道
}()
go func() {
for job := range jobs { // 接收上面的 goroutine 传来的 job (从 jobs 通道)
job.Id = 1
job.Name = "Name"
fmt.Println(job)
done <- true // 发送完成表示等待主 goroutine 接收
}
}()
for i := 0; i < len(jobList); i++ {
<- done // 阻塞,等待接收
}
// 对于通道的使用,我们有两个经验。
// 一. 我们只有在后面要检查通道是否关闭(例如在一个 for ... range 循环
// 里,或者 select, 或者使用 <- 操作符来检查是否可以接收等)的时候才需要
// 显式地关闭通道;
// 二. 应该由发送端的 goroutine 关闭通道,而不是由接收端的 goroutine 来完成
}
Actor
Actor 是一种计算实体,它会对收到的消息做出回应,并且可以做下列事情:
- 向其他 Actor 对象发送一定数量的消息
- 创建一定数量的新 Actor 对象
- 设定对下一条消息做出的回应方式
Celery
以前的我,使用过一种类似于 Actor 发送消息的并发模式,使用消息队列来做并发,最典型的莫过于使用 rabbitmq
的 celery
一般是这样的,写一个 worker
在一个进程跑着,通过 celery
来调用 worker
Celery is used in production systems to process millions of tasks a day.
# __init__.py
from celery_demo.worker import demo_worker
# celery_app.py
# celery -A celery_demo.worker.celery_app worker -l info
from celery import Celery
RABBITMQ_DSN = 'amqp://[email protected]'
app = Celery('worker',
broker=RABBITMQ_DSN,
include=['celery_demo.worker.demo_worker'])
# demo_worker.py
import time
import log
from celery_demo.worker.celery_app import app
@app.task
def add(x, y):
time.sleep(3)
result = x + y
log.app.info(f'result: {result}')
return result
# demo.py
from celery_demo.worker import demo_worker
import log
if __name__ == '__main__':
demo_worker.add.delay(1, 2)
log.app.info('end')
# log
[INFO app 2017-09-09 11:53:28,992] end
[INFO app 2017-09-09 11:53:31,997] result: 3
Akka
Akka is a toolkit for building highly concurrent, distributed, and resilient message-driven applications for Java and Scala
- Simpler Concurrent & Distributed Systems
- Resilient by Design
- High Performance
- Elastic & Decentralized
- Reactive Streaming Data
package io.binglau.scala.akka.demo
import akka.actor.{Actor, ActorSystem, Props}
import akka.event.Logging
class ActorPrint extends Actor {
val log = Logging(context.system, this)
override def receive: Receive = {
case msg : String => log.info("receive {}", msg)
case _ => log.info("receive unknow message")
}
}
object ActorDemo {
def main(args: Array[String]): Unit = {
val system = ActorSystem("demo")
val printActor = system.actorOf(Props[ActorPrint], "print")
printActor ! "Test"
printActor ! 123
system.terminate()
}
}
Actor 系统和 Actor 对象具有的特点:
-
直接通过异步消息传递方式进行通信
同一个 Actor 对象发送保证顺序,但是不同的 Actor 对象之间的发送不能保证顺序
-
状态机
当 Actor 对象转换为某个预设状态时,就能够改变对未来接收到的消息的处理模式。通过变为另一种消息处理器,Actor对象就成了一种有限状态机。
-
无共享
一个 Actor 对象不会与其他 Actor 对象或相关组件共享可变状态
-
无锁的并发处理方式
因为 Actor 对象不会共享它们的可变状态,而且它们在同一时刻仅会接收一条消息,所以在对消息做出回应前,Actor 对象永远都不需要尝试锁定它们的状态。
-
并行性
当等级较高的 Actor 对象能够将多个任务分派给多个下级 Actor 对象,或者任务重含有复杂的处理层级时,就适合通过 Actor 模型使用并行处理方式。
-
Actor 对象的系统性
Actor 对象的量级都非常轻,因此在单个系统中添加许多 Actor 对象是受推荐的处理方式。任何问题都可以通过添加 Actor 对象来解决。
参考书籍
Clojure并发
Akka 官方文档
《七周七并发模式》
《响应式架构——消息模式 Actor 实现与 Scala、Akka 应用集成》
《Go 语言程序设计》
Redis与消息队列
背景知识
Pub/Sub
Redis中使用SUBSCRIBE, UNSUBSCRIBE 和 PUBLISH 方法将可以实现生产者/消费者模式,所谓的生产者其实就是使用PUBLISH
将message(这里需要说明,message只能是String,不过我们可以序列化一个对象为Json再进行推送)推送到指定的队列中,再由SUBSCRIBE
监听到消息并取出。UNSUBSCRIBE
是取消监听。
具体来说我们可以打开两个redis-cli来进行交互
127.0.0.1:6379> SUBSCRIBE first_channel second_channel
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "first_channel"
3) (integer) 1
1) "subscribe"
2) "second_channel"
3) (integer) 2
1) "message"
2) "first_channel"
3) "first_message"
1) "message"
2) "second_channel"
3) "second_message"
127.0.0.1:6379[1]> PUBLISH first_channel first_message
(integer) 1
127.0.0.1:6379[1]> PUBLISH second_channel second_message
(integer) 1
127.0.0.1:6379[1]>
这里需要说明的是SUBSCRIBE
可以同时监听多个队列,这里对我们实现消息队列中的Topic
是至关重要的。
功能实现
这里我们使用Python
实现所有的代码
订阅者
#!/usr/bin/env python
# coding=utf-8
import redis
class SubChannel(object):
def __init__(self, topic):
self.topic = topic
self.redis_conn = redis.Redis(host="localhost", db=1)
self.pubsub = self.redis_conn.pubsub()
self.pubsub.subscribe("%s:channel" % topic)
def run(self):
for i in self.pubsub.listen():
if i['type'] == 'message':
print "Task get", i['data']
if __name__ == "__main__":
SubChannel("Test").run()
发布者
#!/usr/bin/env python
# coding=utf-8
import redis
class PubChannel(object):
def __init__(self, topic):
self.topic = topic
self.redis_conn = redis.Redis(host="localhost", db=1)
self.pubsub = self.redis_conn.pubsub()
def push(self, message):
self.redis_conn.publish("%s:channel" % self.topic, message)
然后我们打开一个Python交互器,我这里使用的是bpython
> bpython ~/code/Python/daily/redis-channel@BingLaudeMacBook-Pro.local
bpython version 0.14.2 on top of Python 2.7.10 /usr/bin/python
>>> from pub import PubChannel
>>> p = PubChannel("Test")
>>> p.push("test")
>>>
> bpython ~/code/Python/daily/redis-channel@BingLaudeMacBook-Pro.local
bpython version 0.14.2 on top of Python 2.7.10 /usr/bin/python
>>> from pub import PubChannel
>>> p = PubChannel("Test")
>>> p.push("test")
>>> p = PubChannel("Test1")
>>> p.push("test")
# 结果
Task get test
这里我们可以设置不同的Topic来实现推送不同类型的消息给不同的订阅者
持久化
由于Redis这种队列不能进行持久化,所以我们需要借助其他的存储来实现持久化,如我们可以使用Mysql
来记录推送的消息,然后在队列消息消耗完成之后将其值置为已完成的状态。
对于Mysql里面的数据,我们可以设置一个消耗时间,如果在某固定时间间隔内消息没有被消耗,我们可以采取相应的操作,如报警,重发等等。
最后
Redis实现消息队列还可以使用List来实现, 这种方法主要是依赖BLPOP
(删除和获取列表中的第一个元素,或阻塞直到有可用) BRPOP
(删除和获取列表中的最后一个元素,或阻塞直到有可用)这两个命令来实现。下面这篇文章很好的讲解了如何用这两个命令来实现消息队列,并附有优先级的实现方法。
Spring-源码解析-容器的功能扩展-后续
本文承接自: Spring-源码解析-容器的功能扩展-BeanFactory功能扩展
依照前文继续分析: AbstractApplicationContext#refresh()
BeanFactory 的后处理
BeanFacotry 作为 Spring 中容器功能的基础,用于存放所有已经加载的 bean,为了保证程序上的高可扩展性,Spring 针对 BeanFactory 做了大量的扩展,比如我们熟知的 PostProcessor 等都是在这里实现的。
激活注册的 BeanFactoryPostProcessor
BeanFactoryPostProcessor
接口跟 BeanPostProcessor
类似,可以对 bean 的定义(配置元数据)进行处理。也就是说,Spring IoC 容器允许 BeanFactoryPostProcessor
在容器实际实例化任何其他的 bean 之前读取配置元数据,并有可能修改它。如果你愿意,你可以配置多个 BeanFactoryPostProcessor
。你还能通过设置 order 属性来控制 BeanFactoryPostProcessor
的执行次序(仅当 BeanFactoryPostProcessor
实现了Ordered
接口时你才可以设置此属性,因此在实现 BeanFactoryPostProcessor
时,就应当考虑实现 Ordered
接口)。
如果你想改变实际的 bean 实例(例如从配置元数据创建的对象),那么你最好使用 BeanPostProcessor
。同样地,BeanFactoryPostProcessor
的作用域范围是容器级的。它只和你所使用的容器有关。如果你在容器中定义一个 BeanFactoryPostProcessor
,它仅仅对此容器中的 bean 进行后置处理。BeanFactoryPostProcessor
不会对定义在另一个容器中的 bean 进行后置处理,即使这两个容器都是在同一层次上。
BeanFactoryPostProcessor 的典型应用:PropertyPlaceholderConfigurer
指定配置文件使用。
PropertyPlaceholderConfigurer
这个类间接继承了 BeanFactoryPostProcessor
接口。这是一个很特别的接口,当 Spring 加载任何实现了这个接口的 bean 的配置时,都会在 bean 工厂载入所有 bean 的配置之后执行 postProcessBeanFactory
方法。在 PropertyResourceConfigurer
类中实现了 postProcessBeanFactory
方法,在方法中先后调用了mergeProperties
、convertProperties
、processProperties
这3个方法,分别得到配置,将得到的配置,将得到的配置转换为合适的类型,最后将配置内容告知 BeanFactory
。
激活 BeanFactoryPostProcessor
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
// Detect a LoadTimeWeaver and prepare for weaving, if found in the meantime
// (e.g. through an @Bean method registered by ConfigurationClassPostProcessor)
if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}
}
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
// Invoke BeanDefinitionRegistryPostProcessors first, if any.
Set<String> processedBeans = new HashSet<String>();
// 对 BeanDefinitionRegistry 类型的处理
if (beanFactory instanceof BeanDefinitionRegistry) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
List<BeanDefinitionRegistryPostProcessor> registryPostProcessors =
new LinkedList<BeanDefinitionRegistryPostProcessor>();
// 硬编码注册的后处理器
// 这里有个疑问,书上应该是 Spring 3 版本应该是在这里调用了 getBeanFactoryPostProcessors()
// 而在当前 4.3.8 版本时候是在外部调用,而且整体函数被抽离
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryPostProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
// 对于 BeanDefinitionRegistryPostPorcessor 类型,在 BeanFactoryPostProcessor
// 的基础 上还有自己定义的方法,需要先调用
registryPostProcessor.postProcessBeanDefinitionRegistry(registry);
registryPostProcessors.add(registryPostProcessor);
}
else {
// 记录常规 BeanFactoryPostProcessor
regularPostProcessors.add(postProcessor);
}
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// Separate between BeanDefinitionRegistryPostProcessors that implement
// PriorityOrdered, Ordered, and the rest.
// 1. 这里是之前篇章提过的在之前功能扩展中注册 ApplicationListenerDetector 的意义所在
// 2. 对于配置中读取的 BeanFactoryPostProcessor 的处理
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
// First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered.
// 首先处理实现 PriorityOrdered 的 BeanDefinitionRegistryPostProcessors
List<BeanDefinitionRegistryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
// 优先级排序
sortPostProcessors(beanFactory, priorityOrderedPostProcessors);
// 加入 registryPostProcessors
registryPostProcessors.addAll(priorityOrderedPostProcessors);
// 激活 BeanDefinitionRegistryPostProcessors 优先级部分
invokeBeanDefinitionRegistryPostProcessors(priorityOrderedPostProcessors, registry);
// Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered.
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
List<BeanDefinitionRegistryPostProcessor> orderedPostProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(beanFactory, orderedPostProcessors);
registryPostProcessors.addAll(orderedPostProcessors);
invokeBeanDefinitionRegistryPostProcessors(orderedPostProcessors, registry);
// Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear.
// 添加其他类型的 BeanDefinitionRegistryPostProcessors
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
BeanDefinitionRegistryPostProcessor pp = beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class);
registryPostProcessors.add(pp);
processedBeans.add(ppName);
pp.postProcessBeanDefinitionRegistry(registry);
reiterate = true;
}
}
}
// Now, invoke the postProcessBeanFactory callback of all processors handled so far.
// 激活 postProcessBeanFactory 方法,之前激活的是 BeanDefinitionRegistryPostProcessors
invokeBeanFactoryPostProcessors(registryPostProcessors, beanFactory);
// 常规 BeanFactoryPostProcessor
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}
else {
// Invoke factory processors registered with the context instance.
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let the bean factory post-processors apply to them!
// 对于配置读取的 BeanFactoryPostProcessor 的处理
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);
// Separate between BeanFactoryPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
// 对后处理器进行分类
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
// 已经处理过的,处理 BeanDefinitionRegistery 的时候(应该叫已经添加到处理序列中)
if (processedBeans.contains(ppName)) {
// skip - already processed in first phase above
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
// First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered.
// 按优先级进行排序
sortPostProcessors(beanFactory, priorityOrderedPostProcessors);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);
// Next, invoke the BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
// 按 order 进排序
sortPostProcessors(beanFactory, orderedPostProcessors);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);
// Finally, invoke all other BeanFactoryPostProcessors.
// 无序,直接调用
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);
// Clear cached merged bean definitions since the post-processors might have
// modified the original metadata, e.g. replacing placeholders in values...
beanFactory.clearMetadataCache();
}
对于 BeanFactoryPostProcessor
的处理主要分两种情况进行,一个是对于 BeanDefinitionRegistry
类的特殊处理,另一种是对普通的 BeanFactoryPostProcessor
进行处理。而对于每种情况都需要考虑硬编码注入注册的后处理器以及通过配置注入的后处理器。
对于 BeanDefinitionRegistry
类型的处理类的处理主要包括以下内容。
-
对于硬编码注册的后处理器的处理,主要是通过
AbstractApplicationContext
中的添加处理器方法addBeanFactoryPostProcessor
进行添加。添加后的处理器会存放入
beanFactoryPostProcessors
中,而在处理BeanFactoryPostProcessor
时候会首先检测beanFactoryPostProcessor
是否有数据。当然,BeanDefinitionRegistryPostProcessor
继承自BeanFactoryPostProcessor
,不但有BeanFactoryPostProcessor
的特性,同事还有自己定义的个性化方法,也需要在此调用。所以,这里需要从beanFactoryPostProcessors
中挑出BeanDefinitionRegistryPostProcessor
的后处理器,并进行其postProcessBeanDefinitionRegistry
方法的激活。 -
记录后处理器主要使用了三个 List 完成
registryPostProcessors
:记录通过硬编码方式注册的BeanDefinitionRegistryPostProcessor
类型的处理器。regularPostProcessors
:记录通过硬编码方式注册的BeanFactoryPostProcessor
类型的处理器。registryPostProcessorBeans
:记录通过配置方式注册的BeanDefinitionRegistryPostProcessor
类型的处理器。
-
对以上所记录的 List 中的后处理器进行统一调用
BeanFactoryPostProcessor
的postProcessBeanFactory
方法。 -
对
beanFactoryPostProcessors
中非BeanDefinitionRegistryPostProcessor
类型的后处理器进行统一的BeanFactoryPostProcessor
的postProcessBeanFactory
方法调用。 -
普通 beanFactory 处理。
注册 BeanPostProcessor
真正的调用是在 bean 的实例化阶段进行的。
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);
// Register BeanPostProcessorChecker that logs an info message when
// a bean is created during BeanPostProcessor instantiation, i.e. when
// a bean is not eligible for getting processed by all BeanPostProcessors.
/**
* BeanPostProcessorChecker 是一个普通的信息打印,可能会有些情况,
* 当 Spring 的配置中的后处理器还没有被注册就已经开始了 bean 的初始化时
* 便会打印出 BeanPostProcessorChecker 中设定的信息
**/
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));
// Separate between BeanPostProcessors that implement PriorityOrdered,
// Ordered, and the rest.
// 使用 PriorityOrdered 接口保证顺序
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
// MergedBeanDefinitionPostProcessor
List<BeanPostProcessor> internalPostProcessors = new ArrayList<BeanPostProcessor>();
// 使用 Ordered 接口保证顺序
List<String> orderedPostProcessorNames = new ArrayList<String>();
// 无序 BeanPostProcessor
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}
// First, register the BeanPostProcessors that implement PriorityOrdered.
// 注册所有实现 PriorityOrdered 的 BeanPostProcessors
sortPostProcessors(beanFactory, priorityOrderedPostProcessors);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);
// Next, register the BeanPostProcessors that implement Ordered.
// 注册所有实现 Ordered 的 BeanPostProcessors
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(beanFactory, orderedPostProcessors);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);
// Now, register all regular BeanPostProcessors.
// 注册所有无序的 BeanPostProcessors
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);
// Finally, re-register all internal BeanPostProcessors.
// 注册所有 MergedBeanDefinitionPostProcessor 类型的 BeanPostProcessor, 并非重复注册
// 在 beanFactory.addBeanPostProcessor 中会先移除以及存在的 BeanPostProcessor
sortPostProcessors(beanFactory, internalPostProcessors);
registerBeanPostProcessors(beanFactory, internalPostProcessors);
// Re-register post-processor for detecting inner beans as ApplicationListeners,
// moving it to the end of the processor chain (for picking up proxies etc).
// 添加 ApplicationListener 探测器
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}
对比 BeanFactoryPostProcessor
的处理要区分两种情况,一种方式是通过硬编码方式的处理,另一种是通过配置文件方式的处理。
对于 BeanFactoryPostProcessor
的处理,不但要实现注册功能,而且还要实现对后处理器的激活操作,所以需要载入配置中的定义,并进行激活;而对于 BeanPostProcessor
并不需要马上调用,再说,硬编码的方式实现的功能是将后处理器提取并调用,这里并不需要调用,当然不需要考虑硬编码的方式了,这里的功能只需要将配置文件的 BeanPostProcessor
提取出来并注册进入 beanFactory 就可以了。
初始化 ApplicationEventMulticaster
具体使用
-
定义事件
public class TestEvent extends ApplicationEvent { public String msg; public TestEvent(Object source) { super(source); } public TestEvent(Object source, String msg) { super(source); this.msg = msg; } public void print() { System.out.println(msg); } }
-
定义监听器
public class TestListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof TestEvent) { TestEvent testEvent = (TestEvent) event; testEvent.print(); } } }
<bean id="testListener" class="io.github.binglau.TestListener" />
-
测试
@Test public void testEvent() { ApplicationContext ctx = new ClassPathXmlApplicationContext("beanFactory.xml"); TestEvent event = new TestEvent("hello", "msg"); ctx.publishEvent(event); }
具体实现
initApplicationEventMulticaster
的方式比较简单,无非考虑两种情况。
- 如果用户自定义了事件广播器,那么使用用户自定义的事件广播器。
- 如果用户没有自定义事件广播器,那么使用默认的
ApplicationEventMulticaster
。
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isDebugEnabled()) {
logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
"': using default [" + this.applicationEventMulticaster + "]");
}
}
}
由此推断其中默认实现的广播器 SimpleApplicationEventMulticaster
中有逻辑来存储监听器并在合适的时候调用。
public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
Executor executor = getTaskExecutor();
if (executor != null) {
executor.execute(new Runnable() {
@Override
public void run() {
invokeListener(listener, event);
}
});
}
else {
invokeListener(listener, event);
}
}
}
可以推断,当产生 Spring 事件的时候会默认使用 SimpleApplicationEventMulticaster
的 multicastEvent
来广播事件,遍历所有监听器,并使用监听器中的 onApplicationEvent
方法来进行监听器的处理。而对于每个监听器来说其实都可以获取到产生的事件,但是是否进行处理则由事件监听器来决定。
注册监听器
protected void registerListeners() {
// Register statically specified listeners first.
// 硬编码方式注册的监听器处理
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}
// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
// 配置文件注册的监听器处理
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}
// Publish early application events now that we finally have a multicaster...
// 提前发布一些消息告诉我们已经有了一个广播器
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}
初始化非延迟加载单例
完成 BeanFactory 的初始化工作,其中包括 ConversionService 的设置、配置冻结以及非延迟加载的 bean 的初始化工作。
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}
// Register a default embedded value resolver if no bean post-processor
// (such as a PropertyPlaceholderConfigurer bean) registered any before:
// at this point, primarily for resolution in annotation attribute values.
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
return getEnvironment().resolvePlaceholders(strVal);
}
});
}
// Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early.
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}
// Stop using the temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(null);
// Allow for caching all bean definition metadata, not expecting further changes.
// 冻结所有的 bean 定义,说明注册的 bean 定义将不被修改或任何进一步的处理
beanFactory.freezeConfiguration();
// Instantiate all remaining (non-lazy-init) singletons.
// 初始化剩下的单实例(非惰性)
beanFactory.preInstantiateSingletons();
}
ConversionService 的设置
之前我们提到过使用自定义类型转换器,那么,在Spring中还提供了另一种转换方式:使用 Converter
。
具体使用
-
定义转换器
public class String2DateConverter implements Converter<String, LocalDate> { static DateTimeFormatter formatter; static { formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); } @Override public LocalDate convert(String source) { try { return LocalDate.parse(source, formatter); } catch (Exception e) { return null; } } }
-
注册
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="io.github.binglau.String2DateConverter" /> </list> </property> </bean>
-
测试(方便起见直接调用测试)
@Test public void testConverter() { DefaultConversionService conversionService = new DefaultConversionService(); conversionService.addConverter(new String2DateConverter()); LocalDate date = conversionService.convert("2017-09-22", LocalDate.class); System.out.println(date); }
冻结配置
@Override
public void freezeConfiguration() {
this.configurationFrozen = true;
this.frozenBeanDefinitionNames = StringUtils.toStringArray(this.beanDefinitionNames);
}
初始化非延迟加载
ApplicationContext
实现的默认行为就是在启动时将所有单例 bean 提前进行实例化。提前实例化意味着作为初始化过程的一部分,ApplicationContext
实例会创建并配置所有的单例 bean。通常情况下这是一件好事,因为这样在配置中的任何错误就会即刻被发现(否则的话可能要花几个小时甚至几天)。而这个实例化的过程就是在 finishBeanFactoryInitialization
中完成的。
@Override
public void preInstantiateSingletons() throws BeansException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Pre-instantiating singletons in " + this);
}
// Iterate over a copy to allow for init methods which in turn register new bean definitions.
// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);
// Trigger initialization of all non-lazy singleton beans...
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return ((SmartFactoryBean<?>) factory).isEagerInit();
}
}, getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
else {
getBean(beanName);
}
}
}
// Trigger post-initialization callback for all applicable beans...
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
smartSingleton.afterSingletonsInstantiated();
return null;
}
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}
finishRefresh
在 Spring 中还提供了 Lifecycle
接口,Lifecycle
中包含 start/stop
方法,实现此接口后 Spring 会保证在启动的时候调用其 start 方法开始生命周期,并在 Spring 关闭的时候调用 stop 方法来结束生命周期,通常用来配置后台程序,在启动后一直运行(如对 MQ 进行轮询等)。而 ApplicationContext
的初始化最后正是保证了这一功能的实现。
protected void finishRefresh() {
// Initialize lifecycle processor for this context.
initLifecycleProcessor();
// Propagate refresh to lifecycle processor first.
getLifecycleProcessor().onRefresh();
// Publish the final event.
publishEvent(new ContextRefreshedEvent(this));
// Participate in LiveBeansView MBean, if active.
LiveBeansView.registerApplicationContext(this);
}
initLifecycleProcessor
当 ApplicationContext 启动或停止时,它会通过 LifecycleProcessor
来与所有声明的 bean 的周期做状态检查,而 LifecycleProcessor
的使用前首先需要初始化。
protected void initLifecycleProcessor() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) {
this.lifecycleProcessor =
beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
if (logger.isDebugEnabled()) {
logger.debug("Using LifecycleProcessor [" + this.lifecycleProcessor + "]");
}
}
else {
DefaultLifecycleProcessor defaultProcessor = new DefaultLifecycleProcessor();
defaultProcessor.setBeanFactory(beanFactory);
this.lifecycleProcessor = defaultProcessor;
beanFactory.registerSingleton(LIFECYCLE_PROCESSOR_BEAN_NAME, this.lifecycleProcessor);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate LifecycleProcessor with name '" +
LIFECYCLE_PROCESSOR_BEAN_NAME +
"': using default [" + this.lifecycleProcessor + "]");
}
}
}
onRefresh
启动所有实现了 Lifecycle
接口的 bean
@Override
public void onRefresh() {
startBeans(true);
this.running = true;
}
private void startBeans(boolean autoStartupOnly) {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new HashMap<Integer, LifecycleGroup>();
for (Map.Entry<String, ? extends Lifecycle> entry : lifecycleBeans.entrySet()) {
Lifecycle bean = entry.getValue();
if (!autoStartupOnly || (bean instanceof SmartLifecycle && ((SmartLifecycle) bean).isAutoStartup())) {
int phase = getPhase(bean);
LifecycleGroup group = phases.get(phase);
if (group == null) {
group = new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly);
phases.put(phase, group);
}
group.add(entry.getKey(), bean);
}
}
if (!phases.isEmpty()) {
List<Integer> keys = new ArrayList<Integer>(phases.keySet());
Collections.sort(keys);
for (Integer key : keys) {
phases.get(key).start();
}
}
}
publishEvent
当晚餐 ApplicationContext 初始化的时候,要通过 Spring 中的事件发布机制来发出 ContextRefreshedEvent 事件,以保证对应的监听器可以做进一步的逻辑处理。
protected void publishEvent(Object event, ResolvableType eventType) {
Assert.notNull(event, "Event must not be null");
if (logger.isTraceEnabled()) {
logger.trace("Publishing event in " + getDisplayName() + ": " + event);
}
// Decorate event as an ApplicationEvent if necessary
ApplicationEvent applicationEvent;
if (event instanceof ApplicationEvent) {
applicationEvent = (ApplicationEvent) event;
}
else {
applicationEvent = new PayloadApplicationEvent<Object>(this, event);
if (eventType == null) {
eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType();
}
}
// Multicast right now if possible - or lazily once the multicaster is initialized
if (this.earlyApplicationEvents != null) {
this.earlyApplicationEvents.add(applicationEvent);
}
else {
getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
}
// Publish event via parent context as well...
if (this.parent != null) {
if (this.parent instanceof AbstractApplicationContext) {
((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
}
else {
this.parent.publishEvent(event);
}
}
}
registerApplicationContext
注册 LiveBeansView MBean(如果处于活动状态)
关于 LiveBeansView:
Adapter for live beans view exposure, building a snapshot of current beans and their dependencies from either a local
ApplicationContext
(with a localLiveBeansView
bean definition) or all registered ApplicationContexts (driven by the "spring.liveBeansView.mbeanDomain" environment property).
static void registerApplicationContext(ConfigurableApplicationContext applicationContext) {
String mbeanDomain = applicationContext.getEnvironment().getProperty(MBEAN_DOMAIN_PROPERTY_NAME);
if (mbeanDomain != null) {
synchronized (applicationContexts) {
if (applicationContexts.isEmpty()) {
try {
MBeanServer server = ManagementFactory.getPlatformMBeanServer();
applicationName = applicationContext.getApplicationName();
server.registerMBean(new LiveBeansView(),
new ObjectName(mbeanDomain, MBEAN_APPLICATION_KEY, applicationName));
}
catch (Throwable ex) {
throw new ApplicationContextException("Failed to register LiveBeansView MBean", ex);
}
}
applicationContexts.add(applicationContext);
}
}
}
Spring源码分析-bean的解析(3)
Spring源码分析-bean的解析(3)
当前版本 Spring 4.3.8
自定义标签的解析
自定义标签使用
在很多情况下,当使用默认配置过于繁琐时候,解析工作或许是一个不得不考虑的负担。Spring 提供了可扩展 Scheme 的支持。大概需要以下几个步骤:
-
创建一个需要扩展的组件
package io.github.binglau.bean; import lombok.Data; /** * 文件描述: */ @Data public class User { private String userName; private String email; }
-
定义一个 XSD 文件描述组件内容
<?xml version="1.0" encoding="UTF-8" ?> <xsd:schema xmlns="http://www.binglau.com/schema/user" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.binglau.com/schema/user" elementFormDefault="qualified"> <xsd:element name="user"> <xsd:complexType> <xsd:attribute name="id" type="xsd:string"/> <xsd:attribute name="userName" type="xsd:string"/> <xsd:attribute name="email" type="xsd:string"/> </xsd:complexType> </xsd:element> </xsd:schema>
-
创建一个文件,实现
BeanDefinitionParser
接口,用来解析 XSD 文件中的定义和组件定义package io.github.binglau; import io.github.binglau.bean.User; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.util.StringUtils; import org.w3c.dom.Element; /** * 文件描述: */ public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { // Element 对应的类 @Override protected Class<?> getBeanClass(Element element) { return User.class; } // 从 element 中解析并提取对应的元素 @Override protected void doParse(Element element, BeanDefinitionBuilder builder) { String userName = element.getAttribute("userName"); String email = element.getAttribute("email"); // 将提取的数据放入 BeanDefinitionBuilder 中,待到完成所有 bean 的解析后统一注册到 beanFactory 中 if (StringUtils.hasText(userName)) { builder.addPropertyValue("userName", userName); } if (StringUtils.hasText(email)) { builder.addPropertyValue("email", email); } } }
-
创建一个 Handler 文件,扩展自 NamespaceHandlerSupport,目的是将组件注册到 Spring 容器
package io.github.binglau; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; /** * 文件描述: */ public class MyNamespaceHandler extends NamespaceHandlerSupport { @Override public void init() { registerBeanDefinitionParser("user", new UserBeanDefinitionParser()); } }
-
编写 Spring.handlers 和 Spring.schemas 文件(resources/MATE-INF)
# Spring.handlers http\://www.binglau.com/schema/user=io.github.binglau.MyNamespaceHandler # Spring.schemas http\://www.binglau.com/schema/user.xsd=user-xsd.xsd
测试:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:myname="http://www.binglau.com/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.binglau.com/schema/user http://www.binglau.com/schema/user.xsd">
<myname:user id="testbean" userName="aaa" email="bbb"/>
<bean id="testBean" class="io.github.binglau.bean.TestBean" />
</beans>
package io.github.binglau;
import io.github.binglau.bean.User;
import org.junit.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;
/**
* 文件描述:
*/
public class BeanFactoryTest {
@Test
public void testSimpleLoad() {
BeanFactory context = new XmlBeanFactory(new ClassPathResource("beanFactory.xml"));
User user = (User)context.getBean("testbean");
System.out.println(user);
}
}
/**
结果:
User(userName=aaa, email=bbb)
**/
自定义标签解析
/**
* Parse the elements at the root level in the document:
* "import", "alias", "bean".
* @param root the DOM root element of the document
*/
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
delegate.parseCustomElement(ele);
}
}
}
}
else {
delegate.parseCustomElement(root);
}
}
其中的 delegate.parseCustomElement(ele)
既是对自定义标签的解析
BeanDefinitionParserDelegate.parseCustomElement
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
// containingBd 为父类 bean,对顶层元素的解析应设置为 null
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 获取对应的命名空间
String namespaceUri = getNamespaceURI(ele);
// 根据命名空间找到对应的 NamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 调用自定义的 NamespaceHandler 进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
获取标签的命名空间
直接调用 org.w3c.dom.Node
中的方法
public String getNamespaceURI(Node node) {
return node.getNamespaceURI();
}
提取自定义标签处理器
private final XmlReaderContext readerContext
即 new ClassPathResource("beanFactory.xml")
在 readerContext
初始化的时候其属性 namespaceHandlerResolver
已经被初始化为 DefaultNamespaceHandlerResolver
实例,所以,这里实际调用了 DefaultNamespaceHandlerResolver
的方法。
/**
* Locate the {@link NamespaceHandler} for the supplied namespace URI
* from the configured mappings.
* @param namespaceUri the relevant namespace URI
* @return the located {@link NamespaceHandler}, or {@code null} if none found
*/
@Override
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已有配置的 handler 映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据命名空间找到对应的信息
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
// 已经做过解析的情况,直接从缓存读取
return (NamespaceHandler) handlerOrClassName;
}
else {
// 没有做过解析,则返回的是类路径
String className = (String) handlerOrClassName;
try {
// 使用反射 将类路径转换为类
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 初始化类
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用自定义的 NamespaceHandler 的初始化方法
namespaceHandler.init();
// 记录在缓存
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
namespaceUri + "] not found", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
namespaceUri + "]: problem with handler class file or dependent class", err);
}
}
}
在上面的调用namespaceHandler.init();
中参考之前的自定义标签使用
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
当得到自定义命名空间处理后回马上进行 BeanDefinitionParser
的注册以支持自定义标签
注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析。getHandlerMappings
主要功能就是读取 Spring.handlers
配置文件并将配置文件缓存在 map 中。
/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map<String, Object> getHandlerMappings() {
// 如果没有被缓存则开始进行缓存
if (this.handlerMappings == null) {
synchronized (this) {
if (this.handlerMappings == null) {
try {
// this.handlerMappingsLocation 在构造函数中被初始化为 META-INF/Spring.handlers
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
// 将 Properties 格式文件合并到 Map 格式的 handlerMappings 中
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return this.handlerMappings;
}
标签解析
NamespaceHandlerSupport#parse
/**
* Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is
* registered for that {@link Element}.
*/
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析操作
return findParserForElement(element, parserContext).parse(element, parserContext);
}
/**
* Locates the {@link BeanDefinitionParser} from the register implementations using
* the local name of the supplied {@link Element}.
*/
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取元素名称,也就是 <myname:user> 中的 user,若在上面的示例中,则此时 localName 为 user
String localName = parserContext.getDelegate().getLocalName(element);
// 根据 user 找到对应的解析器,也就是在
// registerBeanDefinitionParser("user", new UserBeanDefinitionParser()) 注册的解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
而对于 parse 方法的处理
AbstractBeanDefinitionParser#parse
@Override
public final BeanDefinition parse(Element element, ParserContext parserContext) {
// 真正的解析工作
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
String id = resolveId(element, definition, parserContext);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error(
"Id is required for element '" + parserContext.getDelegate().getLocalName(element)
+ "' when used as a top-level tag", element);
}
String[] aliases = null;
if (shouldParseNameAsAliases()) {
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
}
// 将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
// 需要通知监听器则进行处理
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
}
catch (BeanDefinitionStoreException ex) {
parserContext.getReaderContext().error(ex.getMessage(), element);
return null;
}
}
return definition;
}
AbstractSingleBeanDefinitionParser#parseInternal
/**
* Creates a {@link BeanDefinitionBuilder} instance for the
* {@link #getBeanClass bean Class} and passes it to the
* {@link #doParse} strategy method.
* @param element the element that is to be parsed into a single BeanDefinition
* @param parserContext the object encapsulating the current state of the parsing process
* @return the BeanDefinition resulting from the parsing of the supplied {@link Element}
* @throws IllegalStateException if the bean {@link Class} returned from
* {@link #getBeanClass(org.w3c.dom.Element)} is {@code null}
* @see #doParse
*/
@Override
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 获取自定义标签中的 class,此时会调用自定义解析器如 UserBeanDefinitionParser 中的 getBeanClass 方法
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
// 若子类没有重写 getBeanClass 方法则尝试检查子类是否重写 getBeanClassName 方法
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
if (parserContext.isNested()) {
// 若存在父类则使用父类的 scope 属性
// Inner bean definition must receive same scope as containing bean.
builder.setScope(parserContext.getContainingBeanDefinition().getScope());
}
if (parserContext.isDefaultLazyInit()) {
// Default-lazy-init applies to custom bean definitions as well.
// 配置延迟加载
builder.setLazyInit(true);
}
// 调用子类重写的 doParse 方法进行解析
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
doParse(element, builder);
}
参考书籍
《Spring源码深度解析》
openresty初学
背景
前段时间由于不想将一些团队必须有的业务逻辑(如封禁IP,各种常规检查,转发等)写于各个业务线上,于是我们就动起了使用lua
在 nginx
做手脚的心思,一开始本来就组长一个人在弄,后面我出于好奇,就『自愿』援助了其中一条业务线的一个适合写在nginx
的功能,这个功能具体不加以描述,总体来说涉及到了过滤请求,查询数据库,分发请求这几个步骤。
另说明,这里只是介绍一下简单的OpenRestry
应用,本片文章并不涉及其太过深入的阐述,其中介绍的方法,库也都是常用的几个方法,而非所有都会有所涉及,希望这篇文章给你带来一些启迪,如果你想了解更多更加希望你能去看官方文档并参与其社区讨论。
OpenResty
简介
OpenResty 是**人章亦春发起的一个开源项目,它的核心是基于 NGINX 的一个 C 模块,该模块将 Lua 语言嵌入到 NGINX 服务器中,并对外提供一套完整 Lua Web 应用开发 API,透明地支持非阻塞 I/O,提供了“轻量级线程”、定时器等等高级抽象,同时围绕这个模块构建了一套完备的测试框架、调试技术以及由 Lua 实现的周边功能库;这个项目的意义在于极大的降低了高性能服务端的开发难度和开发周期,在快节奏的互联网时代这一点极为重要。
这里是infoQ一篇文章的节选介绍。简单来说OpenResty
给我们在nginx
中提供了一个lua
的钩子,方便我们通过lua
在nignx
这一个web入口层实现一些相对nginx
配置的复杂的功能(但由于lua
语言的局限性,我个人不推荐在这里实现太过复杂的业务逻辑)。
ngx_openresty
目前有两大应用目标:
- 通用目的的 web 应用服务器。在这个目标下,现有的 web 应用技术都可以算是和 OpenResty 或多或少有些类似,比如 Nodejs, PHP 等等。ngx_openresty 的性能(包括内存使用和 CPU 效率)算是最大的卖点之一。
- Nginx 的脚本扩展编程,用于构建灵活的 Web 应用网关和 Web 应用防火墙。有些类似的是 NetScaler。其优势在于 Lua 编程带来的巨大灵活性。
Lua入门
Lua处理在Nginx的几个阶段
init_by_lua
应用模块:http
主要用于加载时候的初始化,在nginx重新加载配置文件时候,就会运行。
set_by_lua
应用模块:server, location
主要用于设置变量,常用于计算一个逻辑,然后返回结果,此时不能使用Output API、Control API、Subrequest API、Cosocket API
rewrite_by_lua
应用模块:http, server, location
此处应该与nginx中的NGX_HTTP_REWRITE_PHASE
阶段对应,是在寻找到匹配的location
之后用于修改请求的URI
,在访问前执行。
access_by_lua
应用模块:http, server, location
主要用于访问控制,能收集到大部分变量,类似status需要在log阶段才有。
这条指令运行于nginx access阶段的末尾,因此总是在 allow 和 deny 这样的指令之后运行,虽然它们同属 access 阶段。
content_by_lua
应用模块:location
阶段是所有请求处理阶段中最为重要的一个,运行在这个阶段的配置指令一般都肩负着生成内容(content)并输出HTTP响应。
header_filter_by_lua
应用模块:http, server, location
一般只用于设置Cookie和Headers等。
body_filter_by_lua
应用模块:http, server, location
一般会在一次请求中被调用多次, 因为这是实现基于 HTTP 1.1 chunked 编码的所谓“流式输出”的。
log_by_lua
应用模块:http, server, location
该阶段总是运行在请求结束的时候,用于请求的后续操作,如在共享内存中进行统计数据,如果要高精确的数据统计,应该使用body_filter_by_lua。
Hello World
这里忽略下载步骤,仅仅流程化地提一下 Hello World。
先写下如下的ngixn配置
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
default_type text/html;
content_by_lua '
ngx.say("<p>hello, world</p>")
';
}
}
}
然后在启动nginx时候加入-c
参数指明该配置文件所在位置应该就能顺利启动了(记住是openresty的nginx启动命令)。
获取当前请求
请求信息是被openresty封装在了ngx.req.*
中,文档中有着比较详尽的描述
function get_request_info()
local headers = ngx.req.get_headers() -- 获取头描述,返回一个table
local uri = ngx.var.uri -- 获取其请求的uri
local method = ngx.req.get_method() -- 获取请求方法字符串
local args = ngx.req.get_uri_args() -- 获取uri中的参数,返回一个table
ngx.req.read_body() -- 指明需要读取body,脱离Nginx事件模块阻塞去同步读取请求body
local body = ngx.req.get_body_data() -- 获取请求体
local request_info = {
headers=headers,
uri=uri,
args=args,
method=method,
body=body,
}
return request_info
end
local cjson = require "cjson"
ngx.log(ngx.WARN, cjson.encode(get_request_info()))
nginx.conf
:
worker_processes 1;
error_log logs/error.log;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location /test {
content_by_lua_file /Users/binglau/code/learn/openresty_work/conf/test.lua;
}
}
}
请求:
> curl localhost:8080/test?test=123 -v
* Trying ::1...
* connect to ::1 port 8080 failed: Connection refused
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /test?test=123 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: openresty/1.9.15.1
< Date: Tue, 27 Sep 2016 12:05:52 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
<
* Connection #0 to host localhost left intact
日志:
2016/09/27 20:05:52 [error] 51223#0: *5 [lua] test.lua:23: {"method":"GET","uri":"\/test","args":{"test":"123"},"headers":{"host":"localhost:8080","accept":"*\/*","user-agent":"curl\/7.43.0"}}, client: 127.0.0.1, server: , request: "GET /test?test=123 HTTP/1.1", host: "localhost:8080"
数据库操作
这里简单说一下lua
的数据库操作,与前面的配置类似,就只是介绍:
-
创建一个新Mysql实例:
local mysql = require "resty.mysql" local db, err = mysql:new() if not db then ngx.say("failed to instantiate mysql: ", err) return end
-
发起连接:
local ok, err, errcode, sqlstate = db:connect{ host = "127.0.0.1", port = 3306, database = "ngx_test", user = "ngx_test", password = "ngx_test", max_packet_size = 1024 * 1024 } if not ok then ngx.say("failed to connect: ", err, ": ", errcode, " ", sqlstate) return end
-
执行sql语句
-- syntax: res, err, errcode, sqlstate = db:query(query) -- syntax: res, err, errcode, sqlstate = db:query(query, nrows) res, err, errcode, sqlstate = db:query("insert into cats (name) " .. "values (\'Bob\'),(\'\'),(null)") if not res then ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") return end ngx.say(res.affected_rows, " rows inserted into table cats ", "(last insert id: ", res.insert_id, ")") -- run a select query, expected about 10 rows in -- the result set: res, err, errcode, sqlstate = db:query("select * from cats order by id asc", 10) if not res then ngx.say("bad result: ", err, ": ", errcode, ": ", sqlstate, ".") return end
-
长连接操作
-- put it into the connection pool of size 100, -- with 10 seconds max idle timeout local ok, err = db:set_keepalive(10000, 100) if not ok then ngx.say("failed to set keepalive: ", err) return end
-
简单防止注入
local name = ngx.unescape_uri(ngx.var.arg_name) local quoted_name = ngx.quote_sql_str(name) local sql = "select * from users where name = " .. quoted_name
发送请求
这里先介绍一下一个openresty
的内置方法capture
-- syntax: res = ngx.location.capture(uri, options?)
-- context: rewrite_by_lua*, access_by_lua*, content_by_lua*
location /test {
content_by_lua_block {
local res = ngx.location.capture(
'/print_param',
{
method = ngx.HTTP_POST,
args = ngx.encode_args({a = 1, b = '2&'}),
body = ngx.encode_args({c = 3, d = '4&'})
}
)
ngx.say(res.body)
}
}
这里的ngx.location.capture
其实是发起一个子请求,而不是一个真正的 http 请求,在搜索如何发起一个新请求的时候大多数人都会告诉你一个库lua-resty-http。
http {
server {
listen 80;
location /test {
content_by_lua_block {
ngx.req.read_body()
local args, err = ngx.req.get_uri_args()
local http = require "resty.http" -- ①
local httpc = http.new()
local res, err = httpc:request_uri( -- ②
"http://127.0.0.1:81/spe_md5",
{
method = "POST",
body = args.data,
}
)
if 200 ~= res.status then
ngx.exit(res.status)
end
if args.key == res.body then
ngx.say("valid request")
else
ngx.say("invalid request")
end
}
}
}
server {
listen 81;
location /spe_md5 {
content_by_lua_block {
ngx.req.read_body()
local data = ngx.req.get_body_data()
ngx.print(ngx.md5(data .. "*&^%$#$^&kjtrKUYG"))
}
}
}
}
这样你是可以发起一个新请求,起初我也是打算用这个库,最终看其文档始终不能完成其中的连接池功能,于是寻求到了另一个方法。
利用proxy_pass
发起,请求到另外一个上游。
http {
upstream md5_server{
server 127.0.0.1:81;
keepalive 20;
}
server {
listen 80;
location /test {
content_by_lua_block {
ngx.req.read_body()
local args, err = ngx.req.get_uri_args()
local res = ngx.location.capture('/spe_md5',
{
method = ngx.HTTP_POST,
body = args.data
}
)
if 200 ~= res.status then
ngx.exit(res.status)
end
if args.key == res.body then
ngx.say("valid request")
else
ngx.say("invalid request")
end
}
}
location /spe_md5 {
proxy_pass http://md5_server;
}
}
server {
listen 81;
location /spe_md5 {
content_by_lua_block {
ngx.req.read_body()
local data = ngx.req.get_body_data()
ngx.print(ngx.md5(data .. "*&^%$#$^&kjtrKUYG"))
}
}
}
}
但是这样一来我们每一个子请求几乎都需要配置一个proxy_pass
,不过我在网上又找到了一个方法
location /proxy/ {
internal;
rewrite ^/proxy/(https?)/([^/]+)/(.*) /$3 break;
proxy_pass $1://$2;
}
通过rewrite
的分组重写,我们动态配置proxy_pass
,这样如果是请求http://www.example.com/foo/bar
就只需要在lua代码中构造一个请求/proxy/http/www.example.com/foo/bar
即可。
参考资料
Java 内存模型与线程
预热
所谓并发的时代,其实不仅仅提现在并发友好的库、框架、语言在兴起,而是这种并发的**,早已融入到计算机最底层中了。
『让计算机并发执行若干个运算任务』与『更充分地利用计算机处理器的效能』之间的因果关系,看起来顺理成章,实际上它们之间的关系并没有想象中的那么简单,其中一个重要的复杂性来源是绝大多数的运算任务都不可能只靠处理器『计算』就能完成,处理器至少要与内存交互,如读取运算数据、存储运算结果等,这个I/O操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。
由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。
嫌上面文字太长了,简单说来就是:计算机计算太快了,等不及慢的主存了,加了 L3、L2、L1 以及寄存器4个高速缓存就是为了让 IO 能快点。
由此产生出了一个问题
缓存一致性
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory),如图12-1所示。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及DragonProtocol等。
什么是内存模型
内存模型可以理解为在特定的操作协议(上述协议)下,对特定的内存或高速缓存进行读写访问的过程抽象。
不同架构的物理机器可以拥有不一样的内存模型,而Java虚拟机也有自己的内存模型,并且这里介绍的内存访问操作与硬件的缓存访问操作具有很高的可比性。
重排序
除了增加高速缓存之外,为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此,如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。
与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。
对处理器的重排序感兴趣的同学可以去看看《深入理解计算机系统》的第 4 章 处理器体系架构
Java 内存模型
之所以有 Java 内存模型(Java Memory Model,JMM)存在的意义是为了屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果。
主内存与工作内存
Java 内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,Java 内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。
Java 内存模型规定了所有的变量都存储在主内存(MainMemory)中(非系统主内存,虚拟机内存一部分)。每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图所示。
内存见交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了以下8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(对于 double 和 long 类型的变量来说,load、store、read和write操作在某些平台上允许有例外)。
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
略解释一下以上的操作:
- 把一个变量从主内存复制到工作内存: 顺序执行 read、 load
- 把一个变量从工作内存同步回主内存:顺序执行 store、 write
注意**『顺序执行』不代表是连续执行**。
除此之外,Java 内存模型还规定了在执行上述 8 种基本操作时必须满足如下规则:
- 不允许 read 和 load、store 和 write 操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。
- 不允许一个线程丢弃它的最近的 assign 操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
- 不允许一个线程无原因地(没有发生过任何 assign 操作)把数据从线程的工作内存同步回主内存中。
- 一个新的变量只能在主内存中『诞生』,不允许在工作内存中直接使用一个未被初始化( load 或 assign )的变量,换句话说,就是对一个变量实施 use、store 操作之前,必须先执行过了 assign 和 load 操作。
- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
- 如果对一个变量执行 lock 操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。
- 如果一个变量事先没有被 lock 操作锁定,那就不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定住的变量。
- 对一个变量执行 unlock 操作之前,必须先把此变量同步回主内存中(执行 store、write 操作)。
volatile
当一个变量定义为 volatile 之后,它将具备两种特性
可见性
保证此变量对所有线程的可见性,这里的『可见性』是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。
可见性常被误解,认为以下描述成立:『volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立刻反应到其他线程之中,换句话说,volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的』。这句话的论据部分并没有错,但是其论据并不能得出『基于volatile变量的运算在并发下是安全的』。
volatile 变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。我们可以看看下面这个实例。
public class VolatileTest {
public static volatile int race = 0;
public static void increaes() {
race++;
}
private static final int THREADES_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADES_COUNT];
for (int i = 0; i <THREADES_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increaes();
}
}
});
threads[i].start();
}
// 等待所有累加线程都结束
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
这段代码发起了 20 个线程,每个对 race 进行了 10000 次自增操作,如果这段代码正确运行的话,结果应该是 200000 ,但总是会事与愿违。
这儿有一点特别奇怪,我在 idea 上面运行上述代码发现一直不能停止,后面试了试 Thread.activeCount() 的值,发现我这边不开启任何子线程数值都是 2。然后将当前线程组打印出来。
System.out.println(Thread.activeCount()); ThreadGroup group = Thread.currentThread().getThreadGroup(); group.list();结果如下
2 java.lang.ThreadGroup[name=main,maxpri=10] Thread[main,5,main] Thread[Monitor Ctrl-Break,5,main]
然后根据去 Monitor Ctrl-Break 查资料发现
java 实际开始运行的类是com.intellij.rt.execution.application.AppMain。AppMain 通过反射调用我们写的类,还会创建另一个名叫 Monitor Ctrl-Break 的线程,从名字看应该是监控 Ctrl-Break 中断信号的。
https://www.zhihu.com/question/59297272
还蛮有趣的
问题就出现在自增运算『race++』之中,我们用 Javap 反编译这段代码后会得到下面的代码,发现只有一行代码的increase()
方法在 Class 文件中是由4条字节码指令构成的(return 指令不是由 race++ 产生的,这条指令可以不计算),从字节码层面上很容易就分析出并发失败的原因了:当 getstatic 指令把 race 的值取到操作栈顶时,volatile 关键字保证了 race 的值在此时是正确的,但是在执行 iconst_1、iadd 这些指令的时候,其他线程可能已经把 race 的值加大了,而在操作栈顶的值就变成了过期的数据,所以 putstatic 指令执行后就可能把较小的 race 值同步回主内存之中。
public static void increaes();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field race:I
3: iconst_1
4: iadd
5: putstatic #2 // Field race:I
8: return
LineNumberTable:
line 11: 0
line 12: 8
客观来说这样也不严谨,因为字节码的一个指令也并不意味这是一个院子操作。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用 synchronized 或 java.util.concurrent 中的原子类)来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
禁止指令重排序优化
可查看文章开头的 volatile变量法则
使用 volatile 变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics)。
上述有些不太好理解,那么我们就看实例吧
Map configOptions;
char[] configText;
// 此变量必须定义为 volatile
volatile boolean initialized = false;
// 假设以下代码在线程 A 中执行
// 模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// 假设以下代码在线程B中执行
// 等待initialized为true,代表线程A已经把配置信息初始化完成
while (!initialized) {
sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();
如果定义 initialized 变量时没有使用 volatile 修饰,就可能会由于指令重排序的优化,导致位于线程 A 中最后一句的代码initialized=true
被提前执行(这里虽然使用 Java 作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这句话对应的汇编代码被提前执行),这样在线程 B 中使用配置信息的代码就可能出现错误,而 volatile 关键字则可以避免此类情况的发生。
下面这个例子说明一下 volatile 关键字是如何禁止指令重排序优化的。下面是一段标准的 DCL 单例代码,可观察加入 volatile 和未加入 volatile 关键字时所生成汇编代码的差别。(使用 HSDIS )(我们就通过行数找)
加入前:
0x00000001086f3207: movabs $0x107ba5abe,%r10
0x00000001086f3211: callq *%r10 ;*monitorexit
; - VolatileSingleton::getInstance@28 (line 14)
0x00000001086f3214: jmpq 0x00000001086f2fe6 ;*new
; - VolatileSingleton::getInstance@17 (line 12)
0x00000001086f3219: mov %rax,%rbx
0x00000001086f321c: jmp 0x00000001086f3221 ;*invokespecial <init>
; - VolatileSingleton::getInstance@21 (line 12)
0x00000001086f321e: mov %rax,%rbx
加入后:
0x000000010eff4100: mov (%rsp),%r10
0x000000010eff4104: shr $0x3,%r10
0x000000010eff4108: movabs $0x76ab81e48,%r11 ; {oop(a 'java/lang/Class' = 'VolatileSingleton')}
0x000000010eff4112: mov %r10d,0x68(%r11)
0x000000010eff4116: movabs $0x76ab81e48,%r10 ; {oop(a 'java/lang/Class' = 'VolatileSingleton')}
0x000000010eff4120: shr $0x9,%r10
0x000000010eff4124: movabs $0x1038b7000,%r11
0x000000010eff412e: mov %r12b,(%r11,%r10,1)
0x000000010eff4132: lock addl $0x0,(%rsp) ;*putstatic instance
; - VolatileSingleton::getInstance@24 (line 12)
虽然看不是很懂,但是还是很明显有一个重排序的区别以及 lock addl $0x0,(%rsp)
这个操作在赋值后(mov (%rsp),%r10
)的区别。
这个操作相当于一个内存屏障(Memory Barrier 或 Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个 CPU 访问内存时,并不需要内存屏障;但如果有两个或更多CPU访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。
内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障可以被分为以下几种类型
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。为了实现上一章中讨论的JSR-133的规定,Java编译器会这样使用内存屏障。
在某些情况下,volatile 的同步机制的性能确实要优于锁(使用 synchronized 关键字或 java.util.concurrent 包里面的锁),但是由于虚拟机对锁实行的许多消除和优化,使得我们很难量化地认为 volatile 就会比 synchronized 快多少。如果让volatile自己与自己比较,那可以确定一个原则:volatile 变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下 volatile 的总开销仍然要比锁低,我们在 volatile 与锁之中选择的唯一依据仅仅是 volatile 的语义能否满足使用场景的需求。
JOJO,这是我最后的实例了
最后,我们回顾一下 Java 内存模型中对 volatile 变量定义的特殊规则。假定 T 表示一个线程,V 和 W 分别表示两个 volatile 型变量,那么在进行 read、load、use、assign、store 和 write 操作时需要满足如下规则:
- 只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。(线程中 use 在 load 之后,只有后面出现 use 才能出现 load,use 可以理解与 load、read 必须一起连续出现)。
- 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量V所做的修改)。(线程中 store 在 assign 之后,只有后面出现 store 才能出现 assign,assign 可以理解与 store、write 必须一起连续出现)
- 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或as-sign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
对于 long 和 double 变量的特殊规则
Java内存模型要求 lock、unlock、read、load、assign、use、store、write 这8个操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被 volatile 修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的 load、store、read 和 write 这4个操作的原子性,这点就是所谓的 long 和 double 的非原子性协定(Nonatomic Treatment ofdouble and long Variables)。
Java 内存模型虽然允许虚拟机不把 long 和 double 变量的读写实现成原子操作,但允许虚拟机选择把这些操作实现为具有原子性的操作,而且还『强烈建议』虚拟机这样实现。在实际开发中,目前各种平台下的商用虚拟机几乎都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的 long 和 double 变量专门声明为 volatile。
原子性,可见性与有序性
Java 内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性这 3 个特征来建立的,我们逐个来看一下哪些操作实现了这 3 个特性。
原子性
由 Java 内存模型来直接保证的原子性变量操作包括 read、load、assign、use、store 和 write,我们大致可以认为基本数据类型的访问读写是具备原子性的。
如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java 内存模型还提供了 lock 和 unlock 操作来满足这种需求,尽管虚拟机未把 lock 和 unlock 操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter 和 monitorexit 来隐式地使用这两个操作,这两个字节码指令反映到 Java 代码中就是同步块——synchronized 关键字,因此在 synchronized 块之间的操作也具备原子性。
可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与 volatile 变量的区别是,volatile 的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile 保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。
除了 volatile 之外,Java 还有两个关键字能实现可见性
- synchronized。同步块的可见性是由『对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)』这条规则获得的
- final。被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把『this』的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到『初始化了一半』的对象),那在其他线程中就能看见 final 字段的值。
有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。
前半句是指“线程内表现为串行的语义”(Within-Thread As-If-SerialSemantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 关键字本身就包含了禁止指令重排序的语义,而 synchronized 则是由『一个变量在同一个时刻只允许一条线程对其进行lock操作』这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
介绍完并发中 3 种重要的特性后,读者有没有发现 synchronized 关键字在需要这 3 种特性的时候都可以作为其中一种的解决方案?看起来很『万能』吧。的确,大部分的并发控制操作都能使用 synchronized 来完成。synchronized 的『万能』也间接造就了它被程序员滥用的局面,越『万能』的并发控制,通常会伴随着越大的性能影响。
Java 中重排序 happens-before 规则
Happens-before 的前后两个操作不会被重排序且后者对前者的内存可见。
- 程序次序法则:线程中的每个动作 A 都 happens-before 于该线程中的每一个动作 B,其中,在程序中,所有的动作 B 都能出现在 A 之后。
- 监视器锁法则:对一个监视器锁的解锁 happens-before 于每一个后续对同一监视器锁的加锁。
- volatile变量法则:对 volatile 域的写入操作 happens-before 于每一个后续对同一个域的读写操作。
- 线程启动法则:在一个线程里,对 Thread.start 的调用会 happens-before 于每个启动线程的动作。
- 线程终结法则:线程中的任何动作都 happens-before 于其他线程检测到这个线程已经终结、或者从 Thread.join 调用中成功返回,或 Thread.isAlive 返回 false。
- 中断法则:一个线程调用另一个线程的 interrupt happens-before 于被中断的线程发现中断。
- 终结法则:一个对象的构造函数的结束 happens-before 于这个对象finalizer的开始。
- 传递性:如果 A happens-before 于 B,且 B happens-before 于 C,则 A happens-before 于 C
这个原则非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作之间是否可能存在冲突的所有问题。
实例(『时间上的先后顺序』与『happens-before』之间的不同)
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
一个普通的 bean 操作,存在线程 A 和 B,线程 A 先(时间上的先后)调用了setValue(1)
,然后线程 B 调用了同一个对象的getValue()
,那么线程B收到的返回值是什么?
这里分析一下 happens-before 的各种规则。
- 由于两个方法分别由线程 A 和线程 B 调用,不在一个线程中,所以程序次序规则在这里不适用;
- 由于没有同步块,自然就不会发生 lock 和 unlock 操作,所以管程锁定规则不适用;
- 由于 value 变量没有被 volatile 关键字修饰,所以 volatile 变量规则不适用;
- 后面的线程启动、终止、中断规则和对象终结规则也和这里完全没有关系;
- 因为没有一个适用的先行发生规则,所以最后一条传递性也无从谈起。
因此我们可以判定尽管线程 A 在操作时间上先于线程 B,但是无法确定线程 B 中getValue()
方法的返回结果,换句话说,这里面的操作不是线程安全的。
通过上面的例子,我们可以得出结论:一个操作“时间上的先发生”不代表这个操作会是“先行发生”,那如果一个操作“先行发生”是否就能推导出这个操作必定是『时间上的先发生』呢?很遗憾,这个推论也是不成立的,一个典型的例子就是多次提到的“指令重排序”。看下面这个简单代码块
// 以下操作在同一个线程中执行
int i = 1;
int j = 2;
根据程序次序规则,int i=1
的操作先行发生于 int j=2
,但是 intj=2
的代码完全可能先被处理器执行,这并不影响 happens-before 原则的正确性,因为我们在这条线程之中没有办法感知到这点。
时间先后顺序与 happens-before 原则之间基本没有太大的关系,所以我们衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以 happens-before 原则为准。
Java 与线程
线程的实现
我们知道,线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。
主流的操作系统都提供了线程实现,Java 语言则提供了在不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行 start() 且还未结束的 java.lang.Thread 类的实例就代表了一个线程。我们注意到 Thread 类与大部分的Java API有显著的差别,它的所有关键方法都是声明为 Native 的。在 Java API 中,一个 Native 方法往往意味着这个方法没有使用或无法使用平台无关的手段来实现(当然也可能是为了执行效率而使用 Native 方法,不过,通常最高效率的手段也就是平台相关的手段)。
实现线程主要有3种方式:使用内核线程实现、使用用户线程实现和使用用户线程加轻量级进程混合实现。
使用内核线程实现
内核线程(Kernel-Level Thread, KLT)就是直接由操作系统内核(Kernel,下称内核)支持的线程,这种线程由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就叫做多线程内核(Multi-Threads Kernel)。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process, LWP),轻量级进程就是我们通常意义上所讲的线程,由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。这种轻量级进程与内核线程之间 1:1 的关系称为一对一的线程模型。
由于内核线程的支持,每个轻量级进程都成为一个独立的调度单元,即使有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程继续工作。
但是轻量级进程具有它的局限性:
- 首先,由于是基于内核线程实现的,所以各种线程操作,如创建、析构及同步,都需要进行系统调用。而系统调用的代价相对较高,需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换。
- 其次,每个轻量级进程都需要有一个内核线程的支持,因此轻量级进程要消耗一定的内核资源(如内核线程的栈空间),因此一个系统支持轻量级进程的数量是有限的。
使用用户线程实现
狭义上的用户线程指的是完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现。用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。如果程序实现得当,这种线程不需要切换到内核态,因此操作可以是非常快速且低消耗的,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间 1:N 的关系称为一对多的线程模型。
使用用户线程的优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都需要用户程序自己处理。
使用用户线程加轻量级进程混合实现
线程除了依赖内核线程实现和完全由用户程序自己实现之外,还有一种将内核线程与用户线程一起使用的实现方式。在这种混合实现下,既存在用户线程,也存在轻量级进程。用户线程还是完全建立在用户空间中,因此用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。而操作系统提供支持的轻量级进程则作为用户线程和内核线程之间的桥梁,这样可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险。在这种混合模式中,用户线程与轻量级进程的数量比是不定的,即为N:M的关系
Java 线程的实现
在目前的JDK版本中,操作系统支持怎样的线程模型,在很大程度上决定了 Java 虚拟机的线程是怎样映射的,这点在不同的平台上没有办法达成一致,虚拟机规范中也并未限定 Java 线程需要使用哪种线程模型来实现。线程模型只对线程的并发规模和操作成本产生影响,对 Java 程序的编码和运行过程来说,这些差异都是透明的。
Java SE 最常用的 JVM 是 Oracle/Sun 研发的 HotSpot VM。在这个 JVM 的较新版本所支持的所有平台上,它都是使用 1:1 线程模型的——除了Solaris之外,它是个特例。
这是HotSpot VM在Solaris上所支持的线程模型:Java(TM) and Solaris(TM) Threading
Java 线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度(Cooperative Threads-Scheduling)和抢占式线程调度(Preemptive Threads-Schedul-ing)。Java使用的线程调度方式就是抢占式调度。
协同式线程调度
线程的执行时间由线程本身来控制,线程把自己的工作执行完了之后,要主动通知系统切换到另外一个线程上。
协同式多线程的最大好处是实现简单,而且由于线程要把自己的事情干完后才会进行线程切换,切换操作对线程自己是可知的,所以没有什么线程同步的问题。Lua语言中的“协同例程”就是这类实现。它的坏处也很明显:线程执行时间不可控制,甚至如果一个线程编写有问题,一直不告知系统进行线程切换,那么程序就会一直阻塞在那里。
抢占式线程调度
每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定(在Java中,Thread.yield()可以让出执行时间,但是要获取执行时间的话,线程本身是没有什么办法的)。在这种实现线程调度的方式下,线程的执行时间是系统可控的,也不会有一个线程导致整个进程阻塞的问题。
状态转换
Java语言定义了5种线程状态,在任意一个时间点,一个线程只能有且只有其中的一种状态,这5种状态分别如下。
- New:创建后尚未启动的线程处于这种状态。
- Runable:Runable 包括了操作系统线程状态中的 Running 和 Ready,也就是处于此状态的线程有可能正在执行,也有可能正在等待着CPU为它分配执行时间。
- Waiting:**处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。**以下方法会让线程陷入无限期的等待状态:
- 没有设置Timeout参数的Object.wait()方法。
- 没有设置Timeout参数的Thread.join()方法。
- LockSup-port.park()方法。
- Timed Waiting:处于这种状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间之后它们会由系统自动唤醒。以下方法会让线程进入限期等待状态:
- Thread.sleep()方法。
- 设置了Timeout参数的Object.wait()方法。
- 设置了Timeout参数的Thread.join()方法。
- LockSupport.parkNanos()方法。
- LockSupport.parkUntil()方法。
- Blocked:线程被阻塞了,『阻塞状态』与『等待状态』的区别是:『阻塞状态』在等待着获取到一个排他锁,这个事件将在另外一个线程放弃这个锁的时候发生;而『等待状态』则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
- Terminated:已终止线程的线程状态,线程已经结束执行。
参考文档
《深入理解 Java 虚拟机》
《深入理解计算机系统》
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.