1.Jvm-Sandbox原理分析-Sandbox的码下启动-01
2.spring6ä¸è½½(springå
è´¹ä¸è½½)
3.OpenJDK17-JVM 源码阅读 - ZGC - 并发标记 | 京东物流技术团队
4.从HotSpot源码,深度解读 park 和 unpark
5.我们一起学并发编程:Java内存模型(五)锁的码下内存语义
6.JVM之创建对象源码分析
Jvm-Sandbox原理分析-Sandbox的启动-01
Jvm-Sandbox的启动(一):sandbox.sh脚本分析
Sandbox的启动是通过其内置的shell脚本 sandbox.sh 开始执行的,一切的码下开始皆可从该脚本中探寻出结果。脚本有一定的码下代码量,大概有+行,码下这里将该脚本分为如下几个部分进行讲解:
1、码下php服务端源码变量定义过程这个过程首先预定义了接下来即将使用的码下一些变量。代码如下:
# 定义sandbox的码下home目录,并为其赋值 typeset SANDBOX_HOME_DIR [[ -z ${ SANDBOX_HOME_DIR} ]] && SANDBOX_HOME_DIR=${ PWD}/..# 定义 SANDBOX_USER,码下并为其赋值 typeset SANDBOX_USER=${ USER} [[ -z ${ SANDBOX_USER} ]] && SANDBOX_USER=$(whoami)# 定义 SANDBOX_SERVER_NETWORK typeset SANDBOX_SERVER_NETWORK# 定义lib目录,码下这个目录下主要存放jar包 typeset SANDBOX_LIB_DIR=${ SANDBOX_HOME_DIR}/lib# 定义 SANDBOX_TOKEN_FILE typeset SANDBOX_TOKEN_FILE="${ HOME}/.sandbox.token"# 定义JVM参数 SANDBOX_JVM_OPS typeset SANDBOX_JVM_OPS="-XmsM -XmxM -Xnoclassgc -ea"# 定义目标JVM的码下进程号,后面的码下agent主要attach到该JVM进程上 typeset TARGET_JVM_PID# 定义目标机器IP以及默认机器IP typeset TARGET_SERVER_IP typeset DEFAULT_TARGET_SERVER_IP="0.0.0.0"# 定义目标进程端口 typeset TARGET_SERVER_PORT# 定义名称空间 typeset TARGET_NAMESPACE typeset DEFAULT_NAMESPACE="default"注释和变量命名已经描绘的非常清楚了,在看后面代码遇到忘记了的码下变量可以到这里来回顾下。
这里为其中一些变量补充说明:
SANDBOX_HOME_DIR:shell脚本中,码下-z表示检测紧跟的码下字符串长度是否为0,如果为0返回true。这里使用短路与,如果 ${ SANDBOX_HOME_DIR} 为0,则使用 ${ PWD}/.. 的目录作为sandbox的home目录。这种方式表示优先使用环境变量 SANDBOX_HOME_DIR,如果未定义环境变量SANDBOX_HOME_DIR,则使用当前目录。
SANDBOX_TOKEN_FILE:这个文件主要存放了sandbox attach记录,包括attach进程的host:port。
TARGET_SERVER_IP:一般情况下,我们都是将整个工程打包后上传至目标机器,然后在目标机器上执行该shell脚本,因此默认机器IP一般为localhost即可。
2、执行入口执行入口就比较简单了,就一行代码,其中${ @}会保存我们传递给该shell脚本的所有参数:
main "${ @}"比方说,我们以如下命令启动脚本,异动源码指标则${ @} 就包含了-p 这个参数
./sandbox.sh -p 、main函数main函数是该脚本的重要方法,也是脚本的执行入口,它主要完成了以下几件事:
其代码如下所示:
function main() { # 遍历脚本参数 while getopts "hp:vFfRu:a:A:d:m:I:P:ClSn:X" ARG; do case ${ ARG} in h) # 帮助手册函数,大家可以自行翻阅源码查看 usage exit ;; # 赋值PID p) TARGET_JVM_PID=${ OPTARG} ;; v) OP_VERSION=1 ;; l) OP_MODULE_LIST=1 ;; R) OP_MODULE_RESET=1 ;; F) OP_MODULE_FORCE_FLUSH=1 ;; f) OP_MODULE_FLUSH=1 ;; u) OP_MODULE_UNLOAD=1 ARG_MODULE_UNLOAD=${ OPTARG} ;; a) OP_MODULE_ACTIVE=1 ARG_MODULE_ACTIVE=${ OPTARG} ;; A) OP_MODULE_FROZEN=1 ARG_MODULE_FROZEN=${ OPTARG} ;; d) OP_DEBUG=1 ARG_DEBUG=${ OPTARG} ;; m) OP_MODULE_DETAIL=1 ARG_MODULE_DETAIL=${ OPTARG} ;; # 赋值IP I) TARGET_SERVER_IP=${ OPTARG} ;; # 赋值PORT P) TARGET_SERVER_PORT=${ OPTARG} ;; C) OP_CONNECT_ONLY=1 ;; S) OP_SHUTDOWN=1 ;; n) OP_NAMESPACE=1 ARG_NAMESPACE=${ OPTARG} ;; X) set -x ;; ?) usage exit_on_err 1 ;; esac done # 重置环境 reset_for_env # 校验权限 check_permission# 根据不同的参数,进行相应处理 # 如果没有指定IP,则使用默认值 [ -z "${ TARGET_SERVER_IP}" ] && TARGET_SERVER_IP="${ DEFAULT_TARGET_SERVER_IP}"# 如果没有指定port,使用默认值 [ -z "${ TARGET_SERVER_PORT}" ] && TARGET_SERVER_PORT=0# reset NAMESPACE [[ ${ OP_NAMESPACE} ]] && TARGET_NAMESPACE=${ ARG_NAMESPACE} [[ -z ${ TARGET_NAMESPACE} ]] && TARGET_NAMESPACE=${ DEFAULT_NAMESPACE}if [[ ${ OP_CONNECT_ONLY} ]]; then [[ 0 -eq ${ TARGET_SERVER_PORT} ]] && exit_on_err 1 "server appoint PORT (-P) was missing" SANDBOX_SERVER_NETWORK="${ TARGET_SERVER_IP};${ TARGET_SERVER_PORT}" else # -p was missing [[ -z ${ TARGET_JVM_PID} ]] && exit_on_err 1 "PID (-p) was missing." # attach jvm的核心方法 attach_jvm fi# -v show version [[ -n ${ OP_VERSION} ]] && sandbox_curl_with_exit "sandbox-info/version"# -l list loaded modules [[ -n ${ OP_MODULE_LIST} ]] && sandbox_curl_with_exit "sandbox-module-mgr/list"# -F force flush module [[ -n ${ OP_MODULE_FORCE_FLUSH} ]] && sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=true"# -f flush module [[ -n ${ OP_MODULE_FLUSH} ]] && sandbox_curl_with_exit "sandbox-module-mgr/flush" "&force=false"# -R reset sandbox [[ -n ${ OP_MODULE_RESET} ]] && sandbox_curl_with_exit "sandbox-module-mgr/reset"# -u unload module [[ -n ${ OP_MODULE_UNLOAD} ]] && sandbox_curl_with_exit "sandbox-module-mgr/unload" "&action=unload&ids=${ ARG_MODULE_UNLOAD}"# -a active module [[ -n ${ OP_MODULE_ACTIVE} ]] && sandbox_curl_with_exit "sandbox-module-mgr/active" "&ids=${ ARG_MODULE_ACTIVE}"# -A frozen module [[ -n ${ OP_MODULE_FROZEN} ]] && sandbox_curl_with_exit "sandbox-module-mgr/frozen" "&ids=${ ARG_MODULE_FROZEN}"# -m module detail [[ -n ${ OP_MODULE_DETAIL} ]] && sandbox_curl_with_exit "sandbox-module-mgr/detail" "&id=${ ARG_MODULE_DETAIL}"# -S shutdown [[ -n ${ OP_SHUTDOWN} ]] && sandbox_curl_with_exit "sandbox-control/shutdown"# -d debug if [[ -n ${ OP_DEBUG} ]]; then sandbox_debug_curl "module//post/spring6ä¸è½½(springå è´¹ä¸è½½)
æä¹å¨springå®ç½ä¸ä¸è½½springçjarå ,æºä»£ç åææ¡£?
è¿å ¥springå®ç½âç¹å»ãPROJECTSãèå项âç¹å»Referenceè¿å ¥ä¸è½½âä¸ææ»å¨æ¡æ¾å°ãDistributionZipFilesãâç¹å»ä¸è½½é¾æ¥âéæ©éè¦ççæ¬è¿è¡ä¸è½½å³å¯ã
å ·ä½æä½æ¥éª¤ï¼
1ãæå¼ç¾åº¦æç´¢è¾å ¥springå®ç½ï¼ç¹å»è¿å ¥å ¶å®æ¹ç½ç«ã
2ãè¿å ¥å®ç½åï¼ç¹å»ä¸æ¹å¯¼èªä¸çãPROJECTSãã
3ãè¿å ¥[PROJECTS]?åï¼å³ä¾§ææ¾ç¤ºç®åæ¯è¾é åççæ¬å表ï¼ç¹å»ã4.3.1.3ãd对åºçReferenceè¿å ¥ä¸è½½é¡µé¢ï¼5.0çæ¬ç®åä¸å¯ä»¥ã
4ãç¹å»è¿å ¥è¯¥ä¸è½½é¡µé¢åï¼å¯ä»¥ctrl+fè¾å ¥ãDistributionZipFilesãã
æè æå¨ä¸æIEæ»å¨æ¡æ¾å°ãDistributionZipFilesãï¼ç¹å»ã?ãä¸è½½é¾æ¥ã
5ãç¹å»ä¸è½½é¾æ¥è¿å ¥é¡µé¢åï¼å¯ä»¥çå°ç¸åºçSpringçæ¬ï¼éæ©èªå·±éè¦ççæ¬è¿è¡ä¸è½½ã
6ãæ¤æ¶å¯ä»¥éæ©ä¸ä¸ªçæ¬ï¼ç¹å»è¿å ¥åå¯ä»¥å°ç¸å ³ä¸è½½ï¼ç®åçææ¡£åjaré½å¨ä¸ä¸ªå éã
7ãä¸è½½åçæç»ç»æã
æå¿ è¦ç´æ¥spring6å
å¯ä»¥æ ¹æ®èªå·±çéæ±åSpring6çåè½æ¥å³å®æ¯å¦ä½¿ç¨æ¤çæ¬ï¼å¦æä½ ä½¿ç¨çæ¯Spring5.Xï¼ä¹å¯ä»¥èèå级å°Spring6æ¥è·å¾æ´å¤æ°ç¹æ§ã
spring5å6å·®å«å¤§åspring5å6å·®å«ä¸¤ä¸ªçæ¬å·®å«å¾å¤§ï¼6æ¯ä¸ä¸ªæ°ç产åï¼å¹¶ä¸æ¯5çè¿ä»£ï¼æ以åºå«è¿æ¯å¾å¤§çã
1ãSpring5äºå¹´9æåå¸äºéç¨çæ¬(GA)ï¼å®æ å¿çèªå¹´2æ以æ¥ç¬¬ä¸ä¸ªä¸»è¦SpringFrameworkçæ¬ãå®æä¾äºä¸äºäººä»¬æå¾ å·²ä¹ çæ¹è¿ï¼è¿éç¨äºä¸ç§å ¨æ°çç¼ç¨èä¾ï¼ä»¥ååºå¼å®£è¨ä¸éè¿°çååºå¼åå为åºç¡ãè¿ä¸ªçæ¬æ¯å¾é¿æ¶é´ä»¥æ¥æä»¤äººå ´å¥çSpringFrameworkçæ¬ãSpring5å ¼å®¹Java8åJDK9ï¼å®éæäºååºå¼æµï¼ä»¥ä¾¿æä¾ä¸ç§é¢ è¦æ§æ¹æ³æ¥å®ç°ç«¯ç¹åWebåºç¨ç¨åºå¼åã
2ãSpring6æ¯ä¸ä¸ä¸ªåå¹´çæ°å¼ç«¯ï¼
è°ç»æspring2.5çå ·ä½ä¸è½½å°åçæ¬2.5.6,å®ç½ä¸è½½,ææ°çæ¬æ¯3.0.0.M3,ä¸è¿è¿ä¸æç,ä¸éåå¦ä¹ ,ä½ ç¨2.5.6就好äº
è¿æ¯ä¸è½½é¡µé¢:
ä¸é¢è¿æ¯ä¸è½½é¾æ¥:
è¿ä¸ªæ¯æå ¨ç,springææä¾èµå ³ç³»é½å¨éé¢,è¦å¤M
è¿æ¯ä¸ªspring+å®çææ¡£ç,è¦Må·¦å³
å ¶ä»çæ¬,ä½ è¿å°ä¸è½½é¡µé¢,ç¹é£ä¸ªmoreå°±çå°äº
ps:ä½ å¤ªå¯æäº,è¿ä¹ç®åçæ²¡äººå¸®ä½ ,å¤ç»æåå§
springæäº6è¿å¦5åä¸å¦5ä¹å¯ä»¥ãspringæ¯ä¸ä¸ªå¼æºçJavaåºç¨æ¡æ¶ï¼å®è½å¤å°Javaåºç¨ç¨åºå¼ååå¾æ´å ç®åãå®æä¾äºä¸ä¸ªå¼ºå¤§çæ ¸å¿æ¡æ¶ï¼ä½¿å¼åè è½å¤è½»æ¾å°æ建åºé«è´¨éçåºç¨ç¨åºï¼ä»¥å对å¤é¨ç³»ç»çè½»æ¾è®¿é®ãSpring主è¦æä¸ä¸ªæ¨¡åï¼æ ¸å¿ï¼AOPï¼è¿ç¨ï¼ORMï¼Webï¼æ¶æ¯åæµè¯ãå ¶ä¸ï¼æ ¸å¿æ¨¡åæä¾äºåºç¡çæ¯æï¼èAOP模ååæä¾äºé¢ååé¢ç¼ç¨ï¼AOPï¼çæ¯æï¼ORM模ååæä¾äºå¯¹è±¡å ³ç³»æ å°ï¼ORMï¼çæ¯æï¼èWeb模ååæä¾äºç¨äºæ建åºäºwebçåºç¨ç¨åºçæ¯æãå¦å¤ï¼æ¶æ¯æ¨¡åæä¾äºåºäºJMSçæ¶æ¯æå¡æ¯æï¼èæµè¯æ¨¡ååæä¾äºä¸ä¸ªç®åçæµè¯æ¡æ¶ï¼ç¨äºæ¯æåå æµè¯ã
Spring5æ¯ä¸ä¸ªéè¦ççæ¬ï¼å®å¼å ¥äºå¾å¤æ°çç¹æ§ï¼æ¯å¦ï¼ReactiveProgrammingï¼FunctionalBeanDefinitionsï¼æ¹è¿çJSONæ¯æå对Kotlinçæ¯æãå®è¿æä¾äºå¯¹Java8å9çæ¯æï¼ä»¥å对æ°çJVMè¯è¨ï¼å¦Groovyï¼çæ¯æãæ¤å¤ï¼å®è¿æä¾äºå¯¹Java模åï¼Jigsawï¼çæ¯æï¼ä»¥å对æ°ç容å¨ï¼å¦Dockerï¼çæ¯æã
æ»ä¹ï¼Spring5为å¼åè æä¾äºæ´å¤çåè½ï¼è½å¤å¸®å©å¼åè æ´è½»æ¾å°æ建åºé«è´¨éçåºç¨ç¨åºãSpring6å°ä¼ç»§ç»æä¾æ´å¤çæ°åè½ï¼ä»¥å¸®å©å¼åè æ建åºæ´å¥½çåºç¨ç¨åºã
OpenJDK-JVM 源码阅读 - ZGC - 并发标记 | 京东物流技术团队
ZGC简介:
ZGC是Java垃圾回收器的前沿技术,支持低延迟、大容量堆、染色指针、读屏障等特性,自JDK起作为试验特性,JDK起支持Windows,JDK正式投入生产使用。在JDK中已实现分代收集,预计不久将发布,性能将更优秀。
ZGC特征:
1. 低延迟
2. 大容量堆
3. 染色指针
4. 读屏障
并发标记过程:
ZGC并发标记主要分为三个阶段:初始标记、并发标记/重映射、重分配。本篇主要分析并发标记/重映射部分源代码。
入口与并发标记:
整个ZGC源码入口是ZDriver::gc函数,其中concurrent()是一个宏定义。并发标记函数是concurrent_mark。
并发标记流程:
从ZHeap::heap()进入mark函数,使用任务框架执行任务逻辑在ZMarkTask里,具体执行函数是work。工作逻辑循环从标记条带中取出数据,直到取完或时间到。此循环即为ZGC三色标记主循环。之后进入drain函数,欢乐养猪源码从栈中取出指针进行标记,直到栈排空。标记过程包括从栈取数据,标记和递归标记。
标记与迭代:
标记过程涉及对象迭代遍历。标记流程中,ZGC通过map存储对象地址的finalizable和inc_live信息。map大小约为堆中对象对齐大小的二分之一。接着通过oop_iterate函数对对象中的指针进行迭代,使用ZMarkBarrierOopClosure作为读屏障,实现了指针自愈和防止漏标。
读屏障细节:
ZMarkBarrierOopClosure函数在标记非静态成员变量的指针时触发读屏障。慢路径处理和指针自愈是核心逻辑,慢路径标记指针,快速路径通过cas操作修复坏指针,并重新标记。
重映射过程:
读屏障触发标记后,对象被推入栈中,下次标记循环时取出。ZGC并发标记流程至此结束。
问题回顾:
本文解答了ZGC如何标记指针、三色标记过程、如何防止漏标、指针自愈和并发重映射过程的问题。
扩展思考:
ZGC在指针上标记,当回收某个region时,如何得知对象是否存活?答案需要结合标记阶段和重分配阶段的代码。
结束语:
本文深入分析了ZGC并发标记的源码细节,对您有启发或帮助的话,请多多点赞支持。作者:京东物流 刘家存,来源:京东云开发者社区 自猿其说 Tech。内嵌导航源码转载请注明来源。
从HotSpot源码,深度解读 park 和 unpark
我最近建立了一个在线自习室(App:番茄ToDO)用于相互监督学习,感兴趣的小伙伴可以加入。自习室加入码:D5A7A
Java并发包下的类大多基于AQS(AbstractQueuedSynchronizer)框架实现,而AQS线程安全的实现依赖于两个关键类:Unsafe和LockSupport。
其中,Unsafe主要提供CAS操作(关于CAS,在文章《读懂AtomicInteger源码(多线程专题)》中讲解过),LockSupport主要提供park/unpark操作。实际上,park/unpark操作的最终调用还是基于Unsafe类,因此Unsafe类才是核心。
Unsafe类的实现是由native关键字说明的,这意味着这个方法是原生函数,是用C/C++语言实现的,并被编译成了DLL,由Java去调用。
park函数的作用是将当前调用线程阻塞,而unpark函数则是唤醒指定线程。
park是等待一个许可,unpark是为某线程提供一个许可。如果线程A调用park,除非另一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。每次调用一次park,需要有一个unpark来解锁。
并且,unpark可以先于park调用,但不管unpark先调用多少次,都只提供一个许可,不可叠加。出海系统源码只需要一次park来消费掉unpark带来的许可,再次调用会阻塞。
在Linux系统下,park和unpark是通过Posix线程库pthread中的mutex(互斥量)和condition(条件变量)来实现的。
简单来说,mutex和condition保护了一个叫_counter的信号量。当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。当_counter=0时线程阻塞,当_counter>0时直接设为0并返回。
每个Java线程都有一个Parker实例,Parker类的部分源码如下:
由源码可知,Parker类继承于PlatformParker,实际上是用Posix的mutex和condition来实现的。Parker类里的_counter字段,就是用来记录park和unpark是否需要阻塞的标识。
具体的执行逻辑已经用注释标记在代码中,简要来说,就是检查_counter是不是大于0,如果是,则把_counter设置为0,返回。如果等于零,继续执行,阻塞等待。
unpark直接设置_counter为1,再unlock mutex返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程。源码如下:
(如果不会下载JVM源码可以后台回复“jdk”,获得下载压缩包)
我们一起学并发编程:Java内存模型(五)锁的内存语义
简介:
锁的作用是让临界区互斥执行。本文阐述所得另一个重要知识点——锁的内存语义。
1、锁的释放-获取建立的happens-before关系锁是Java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。
锁释放-获取的示例代码:
packagecom.lizba.p1;/***<p>*锁示例代码*</p>**@Author:Liziba*@Date:/6/:*/publicclassMonitorExample{ inta=0;publicsynchronizedvoidwriter(){ //1;a++;//2;}//3;publicsynchronizedvoidreader(){ //4;inti=a;//5;System.out.println(i);}//6;}假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规范,这个过程包含的happens-before关系可以分为3类。
根据程序次序规则:1happens-before2,2happens-before3,4happens-before5,5happens-before6
根据监视器锁规则:3happens-before4
根据happens-before的传递性,2happens-before5
上述happens-before关系的图形化表现形式如图:
总结:
线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立即变得对B线程可见。
2、锁释放和获取的内存语义当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。以上述MonitorExample程序为例,A线程释放锁后,共享数据的状态示意图如下所示:
共享数据的状态示意图
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器锁保护的临界区代码必须从主内存中读取共享变量。
锁获取的状态示意图
对比锁释放-获取锁的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
总结:
线程A释放锁,实质上是线程A向接下来要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
线程B获取锁,实质上是线程B接受了之前某个线程发出的(在释放这个锁对共享变量锁做的修改的)消息。
线程A是否锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
3、锁内存的语义实现分析ReentrantLock的源代码,来分析锁内存语义的具体实现机制。
示例代码:
packagecom.lizba.p1;importjava.util.concurrent.locks.ReentrantLock;/***<p>*ReentrantLock示例代码*</p>**@Author:Liziba*@Date:/6/:*/publicclassReentrantLockExample{ inta=0;ReentrantLocklock=newReentrantLock();publicvoidwriter(){ lock.lock();//获取锁try{ a++;}finally{ lock.unlock();//释放锁}}publicvoidreader(){ lock.lock();//获取锁try{ inti=a;System.out.println(i);}finally{ lock.unlock();//释放锁}}}在ReentrantLock中,调用lock()方法获取锁;调用unlock()方法释放锁。
ReentrantLock的实现依赖于Java同步器框架AbstractQueuedSynchronized(AQS)。AQS使用一个整型的volatile变量(state)来维护同步状态,这个volatile变量是ReentrantLock内存语义实现的关键。
ReetrantLock的类图
ReentrantLock分为公平锁和非公平锁,首先分析公平锁。
使用公平锁时,加锁方法lock()的调用轨迹如下。
ReentrantLock:lock()
FairSync:lock()
AbstractQueuedSynchronizer:acquire(intarg)
ReentrantLock:tryAcquire(intacquires)
第4步开始真的加锁,下面是该方法的源代码:
protectedfinalbooleantryAcquire(intacquires){ finalThreadcurrent=Thread.currentThread();//获取锁开始,首先读取volatile变量stateintc=getState();if(c==0){ if(!hasQueuedPredecessors()&&compareAndSetState(0,acquires)){ setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){ intnextc=c+acquires;if(nextc<0)thrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}从上面的代码中可以看出,加锁方法首先读取volatile变量state。
在使用公平锁时,解锁方法unlock()调用轨迹如下:
ReentrantLock:unlock()
AbstractQueuedSynchronizer:release(intarg)
Sync:tryRelease(intrelease)
第3步开始真的释放锁,下面是该方法的源代码:
protectedfinalbooleantryRelease(intreleases){ intc=getState()-releases;if(Thread.currentThread()!=getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree=false;if(c==0){ free=true;setExclusiveOwnerThread(null);}//释放锁的最后,写volatile变量statesetState(c);returnfree;}从上面的代码中可以看出,释放锁的最后写volatile变量state。
总结公平锁:
根据volatile的happens-before规则,释放锁的线程在写volatile变量之前可见的共享变量,在获取锁的线程读取到同一个volatile变量后将立即变得对获取锁的线程可见。
现在分析非公平锁:
注意,非公平锁的释放和公平锁的释放完全一致,都是上面的源代码。所以下面只分析非公平锁的获取过程。
使用非公平锁,加锁方法lock()的调用轨迹如下:
ReentrantLock:lock()
NonfairSync:lock()
AbstractQueuedSynchronizer:compareAndSetState(intexpect,intupdate)
第3步开始真的加锁,下面是该方法的源代码:
//方法1finalbooleannonfairTryAcquire(intacquires){ finalThreadcurrent=Thread.currentThread();intc=getState();if(c==0){ //此方法中开始加锁if(compareAndSetState(0,acquires)){ setExclusiveOwnerThread(current);returntrue;}}elseif(current==getExclusiveOwnerThread()){ intnextc=c+acquires;if(nextc<0)//overflowthrownewError("Maximumlockcountexceeded");setState(nextc);returntrue;}returnfalse;}//方法2protectedfinalbooleancompareAndSetState(intexpect,intupdate){ //Seebelowforintrinsicssetuptosupportthis//该方法是native方法,在JVM中实现returnunsafe.compareAndSwapInt(this,stateOffset,expect,update);}该方法以原子操作的方式更新state变量,也就是compareAndSet()(CAS)操作。JDK文档对该方法说明如下:如果当前状态值等于预期值,则以原子方式同步状态设置为给定更新的值。此操作具有volatile读和写的内存语义。
接下来分别从编译器和处理器的角度来分析,CAS如何同时具有volatile读和volatile写的内存语义。
编译器的角度:
前文已经讲过,编译器不会对volatile读与volatile读后面的任意内存操作重排序;编译器不会对volatile写和volatile写后前面的任意内存操作重排序。组合这两个条件,意味着同时实现volatile读和volatile写的内存语义,编译器不能对CAS与CAS前面和后面任意内存操作重排序。
处理器的角度:
(本人不太懂C++)这一块总结需要看JVM源码,可能会总结错误,如需要深入理解这一块请查看《Java并发编程艺术》页。
sun.misc.Unsafe中的compareAndSwapInt源码如下:(不懂Unsafe请看往期文章)
publicfinalnativebooleancompareAndSwapInt(Objectvar1,longvar2,intvar4,intvar5);这是一个本地方法。这个本地方法会在openJDK中调用C++代码,假设当前是X处理器,程序会根据当前处理器的类型来决定是非cmpxchg指令添加lock前缀。如果:
程序运行在多处理器上,就为cmpxchg指令加上lock前缀(LockCmpxchg)
程序运行在单处理器上,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)
intel手册对lock前缀的说明
对内存的读-改-写操作原子执行。(总线锁定/缓存锁定)
禁止该指令,与之前的读和写指令重排序
把写缓冲区的所有数据刷新到内存中
上面的2、3两点所具有的内存屏障的效果,足以同时实现volatile读和volatile写的内存语义。所以JDK文档说CAS具有volatile读和volatile写的内存语义对于处理器也是符合的。
公平锁和非公平锁的总结
公平锁和非公平锁的释放,最后都需要写一个volatile变量state
公平锁获取时,首先会去读volatile变量
非公平锁获取锁时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile写的内存语义
释放锁-获取锁的内存语义的实现方式总结
利用volatile变量的写-读所具有的内存语义
利用CAS所附带的volatile读和volatile写的内存语义
4、concurrent包的实现由于Java的CAS同时具有volatile读和volatile写的内存语义,因此Java线程之间的通信方式有以下4种方式
A线程写volatile变量,随后B线程读这个volatile变量
A线程写volatile变量,随后B线程用CAS更新这个volatile变量
A线程利用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
A线程利用CAS更新一个volatile变量,随后B线程读这个volatile变量
Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,这些原子指令以原子方式对内存执行读-改-写操作,这是在多处理器实现同步的关键。同时volatile变量的读/写和CAS可以实现线程之间的通信。这些特性就是Java整个concurrent包的基石。
concurrent包的通用化实现模式
声明共享变量volatile
使用CAS的原子条件更新来实现线程之间的同步
配合volatile的读/写和CAS具有的volatile读和写的内存语义来实现线程之间的通信。
AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)、非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中基础类都是使用这个模式来实现的,而concurrent包中的高层类又是依赖于这些基础类。
图示concurrent包的实现示意图
concurrent包的实现示意图
文章总结至《Java并发编程艺术》,下篇总结“final域的内存语义”,敬请关注。
JVM之创建对象源码分析
欢迎探索我的技术分享:《半栈工程师》 对于Java对象的创建,我过去只是停留在理论层面,但最近研究HotSpot虚拟机时,我深入剖析了JVM创建Java对象的底层机制。Java对象创建流程详解
首先,我们从一个简单的实例开始,看看如何通过代码创建一个Dog对象: 代码中new Dog()在编译成字节码后,会变成new #2,这里的new是实例化对象的关键字,#2则指向常量池中的Dog类索引。常量池是类编译后的存储区域,包含了各种符号引用和常量。new指令源码剖析
接下来,我们将深入new指令的源码。虽然涉及汇编代码,但无需立即深入,先了解一下《JVM之模板解释器》会有所帮助。新指令的运行过程如下:从指令中获取类在常量池的索引,存入rdx寄存器,并记录当前指令地址。
获取常量池地址和元素类型数组_tags,用于后续类型检查。
检查元素类型是否为JVM_CONSTANT_Class,如果不是,进入慢速分配。
获取并入栈类的运行时数据结构InstanceKlass,即类的内存地址。
判断类是否已解析,未解析则执行慢速分配,解析过的进入快速分配。
计算类实例大小并分配内存,首先尝试TLAB区,失败则在Eden区分配。
初始化对象实例数据和对象头。
如果类未解析,执行慢速分配过程。
总结
至此,我们了解了Java对象从创建到初始化的全过程。虽然使用了模板解释器,但理解字节码解释器中的相关方法也是个不错的选择。如果你对HotSpot源码感兴趣,欢迎加入讨论,我的****是wechat:wang_atbeijing。