jvm源码解读--16 cas 用法解析

时间:2021-08-24 16:15:12

CAS的意思是campare and sweep比较交换

这个如果不用代码会比较抽象,那么在源码中进行解释

void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ; cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
} if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
}

这个就是cas:

  • 参数1:期望更新值
  • 参数2:内存地址,希望更改地址中的值
  • 参数3,预料内存地址中的旧址
  • 返回结果: 返回内存地址中的值,
    • 第一步,将参数1的值放入到rax中,
    • 如果cas更新成功,如果相等,则双向交换 dest 与 exchange_value  返回rax,此时rax为参数1
    • 如果cas失败,否则就单方面地将 dest 指向的内存值交给 ``exchange_value,返回rax,此时rax为参数3

比如以前程序

可以参考:https://www.cnblogs.com/yonghengzh/p/14277544.html

那么通过代码验证

参数1

(gdb) p Self
$6 = (JavaThread * const) 0x7f1e0000b800

参数2

(gdb) p this
$4 = (ObjectMonitor * const) 0x7f1de8002650
(gdb) p * this
$5 = {
static SpinCallbackFunction = 0x0,
static SpinCallbackArgument = 0,
_header = 0x1,
_object = 0xf38068d0,
SharingPad = {-7.4786357953083842e+240},
_owner = 0x0,
_previous_owner_tid = 0,
_recursions = 0,
OwnerIsThread = 1,
_cxq = 0x0,
_EntryList = 0x0,
_succ = 0x0,
_Responsible = 0x0,
_PromptDrain = -235802127,
_Spinner = -235802127,
_SpinFreq = 0,
_SpinClock = 0,
_SpinDuration = 4400,
_SpinState = -1012762419733073423,
_count = 0,
_waiters = 1,
_WaitSet = 0x7f1df0dfabb0,
_WaitSetLock = 0,
_QMix = -235802127,
FreeNext = 0x0,
StatA = -1012762419733073423,
StatsB = -1012762419733073423,
static _sync_ContendedLockAttempts = 0x7f1e0000d928,
static _sync_FutileWakeups = 0x7f1e0000d9f8,
static _sync_Parks = 0x7f1e0000dab8,
static _sync_EmptyNotifications = 0x7f1e0000db78,
static _sync_Notifications = 0x7f1e0000dc38,
static _sync_SlowEnter = 0x7f1e0000dcf8,
static _sync_SlowExit = 0x7f1e0000ddb8,
static _sync_SlowNotify = 0x7f1e0000de78,
static _sync_SlowNotifyAll = 0x7f1e0000df38,
static _sync_FailedSpins = 0x0,
static _sync_SuccessfulSpins = 0x7f1e0000e0b8,
static _sync_PrivateA = 0x7f1e0000e178,
static _sync_PrivateB = 0x7f1e0000e238,
static _sync_MonInCirculation = 0x7f1e0000e2f8,
static _sync_MonScavenged = 0x7f1e0000e3b8,
static _sync_Inflations = 0x7f1e0000d3a8,
static _sync_Deflations = 0x7f1e0000d868,
static _sync_MonExtant = 0x7f1e0000e478,
static Knob_Verbose = 0,
static Knob_SpinLimit = 5000
}

参数3

NULL 为0x0

那么分析如下,如果cas成功的话,预期,&_owner 即参数2的取值为 变为参数1,且返回值为变更后的值 即新值Self

      如果cas失败,预期 参数2的取值不变,返回值为参数2中的取值

inline void*    Atomic::cmpxchg_ptr(void*    exchange_value, volatile void*     dest, void*    compare_value) {
return (void*)cmpxchg((jlong)exchange_value, (volatile jlong*)dest, (jlong)compare_value);
} ==============
inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
bool mp = os::is_MP();
__asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
: "=a" (exchange_value) //输出约束条件
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)//输入约束条件
: "cc", "memory");
return exchange_value;
}

解析内敛汇编

清单 C:内联汇编代码块的组成

__asm__ __volatile__(assembly template

        : output operand list

        : input operand list

        : clobber list

                            );

2. 汇编模板:

  • = 表示该变量是只写的。& 表示这个变量不能与任何输入操作数共享相同的寄存器
  • 字母a是寄存器EAX/AX/AL的缩写,说明cr0的值要从寄存器EAX中获取,也就是说cr0=eax,最终这一点被转化成汇编指令就是:movl %eax,address_of_cr0;
  • 常用的寄存器约束的缩写:
    r:I/O,表示使用一个通用寄存器,由GCC在%eax/%ax/%al、%ebx/%bx/%bl、%ecx/%cx/%cl、%edx/%dx/%dl中选取一个GCC认为是合适的;
    q:I/O,表示使用一个通用寄存器,与r的意义相同;
    g:I/O,表示使用寄存器或内存地址;
    m:I/O,表示使用内存地址;
    a:I/O,表示使用%eax/%ax/%al;
    b:I/O,表示使用%ebx/%bx/%bl;
    c:I/O,表示使用%ecx/%cx/%cl;
    d:I/O,表示使用%edx/%dx/%dl;
    D:I/O,表示使用%edi/%di;
    S:I/O,表示使用%esi/%si;
    f:I/O,表示使用浮点寄存器;
    t:I/O,表示使用第一个浮点寄存器;
    u:I/O,表示使用第二个浮点寄存器;
    A:I/O,表示把%eax与%edx组合成一个64位的整数值;
    o:I/O,表示使用一个内存位置的偏移量;
    V:I/O,表示仅仅使用一个直接内存位置;
    i:I/O,表示使用一个整数类型的立即数;
    n:I/O,表示使用一个带有已知整数值的立即数;
    F:I/O,表示使用一个浮点类型的立即数;
    2.内存约束:
    如果一个Input/Output操作表达式的C/C++表达式表现为一个内存地址(指针变量),不想借助于任何寄存器,则可以使用内存约束;比如:
    __asm__("lidt %0":"=m"(__idt_addr));或__asm__("lidt %0"::"m"(__idt_addr));
    内存约束使用约束名"m",表示的是使用系统支持的任何一种内存方式,不需要借助于寄存器;
    使用内存约束方式进行输入输出时,由于不借助于寄存器,所以,GCC不会按照你的声明对其做任何的输入输出处理;GCC只会直接拿来使用,对这个C/C++表达式而言,究竟是输入还是输出,完全依赖于你写在"instruction list"中的指令对其操作的方式;所以,不管你把操作约束和操作表达式放在Input部分还是放在Output部分,GCC编译生成的汇编代码都是一样的,程序的执行结果也都是正确的;本来我们将一个操作表达式放在Input或Output部分是希望GCC能为我们自动通过寄存器将表达式的值输入或输出;既然对于内存约束类型的操作表达式来说,GCC不会为它做任何事情,那么放在哪里就无所谓了;但是从程序员的角度来看,为了增强代码的可读性,最好能够把它放在符合实际情况的地方;
    3.立即数约束:
    如果一个Input/Output操作表达式的C/C++表达式是一个数字常数,不想借助于任何寄存器或内存,则可以使用立即数约束;
    由于立即数在C/C++表达式中只能作为右值使用,所以,对于使用立即数约束的表达式而言,只能放在Input部分;比如:
    __asm__ __volatile__("movl %0,%%eax"::"i"(100));
    立即数约束使用约束名"i"表示输入表达式是一个整数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;使用约束名"F"表示输入表达式是一个浮点数类型的立即数,不需要借助于任何寄存器,只能用于Input部分;
    4.通用约束:
    约束名"g"可以用于输入和输出,表示可以使用通用寄存器、内存、立即数等任何一种处理方式;
    约束名"0,1,2,3,4,5,6,7,8,9"只能用于输入,表示与第n个操作表达式使用相同的寄存器/内存;
    通用约束"g"是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际操作中,无论使用寄存器方式、内存方式还是立即数方式都无所谓时,或者程序员想实现一个灵活的模板,以让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用通用约束g;
    例如:
    #define JUST_MOV(foo) __asm__("movl %0,%%eax"::"g"(foo))
    则,JUST_MOV(100)和JUST_MOV(var)就会让编译器产生不同的汇编代码;
    对于JUST_MOV(100)的汇编代码为:
    #APP
     movl $100,%eax      #立即数方式;
    #NO_APP
    对于JUST_MOV(var)的汇编代码为:
    #APP
     movl 8(%ebp),%eax   #内存方式;
    #NO_APP
    像这样的效果,就是通用约束g的作用;
    5.修饰符:
    等号(=)和加号(+)作为修饰符,只能用于Output部分;等号(=)表示当前输出表达式的属性为只写,加号(+)表示当前输出表达式的属性为可读可写;这两个修饰符用于约束对输出表达式的操作,它们俩被写在输出表达式的约束部分中,并且只能写在第一个字符的位置;
    符号&也写在输出表达式的约束部分,用于约束寄存器的分配,但是只能写在约束部分的第二个字符的位置上;

    四、占位符
    每一个占位符对应一个Input/Output操作表达式;
    带C/C++表达式的内联汇编中有两种占位符:序号占位符和名称占位符;
    1.序号占位符:
    GCC规定:一个内联汇编语句中最多只能有10个Input/Output操作表达式,这些操作表达式按照他们被列出来的顺序依次赋予编号0到9;对于占位符中的数字而言,与这些编号是对应的;比如:占位符%0对应编号为0的操作表达式,占位符%1对应编号为1的操作表达式,依次类推;
    由于占位符前面要有一个百分号%,为了去边占位符与寄存器,GCC规定:在带有C/C++表达式的内联汇编语句的指令列表里列出的寄存器名称前面必须使用两个百分号(%%),一区别于占位符语法;
    GCC对占位符进行编译的时候,会将每一个占位符替换为对应的Input/Output操作表达式所指定的寄存器/内存/立即数;
    例如:
    __asm__("addl %1,%0\n\t":"=a"(__out):"m"(__in1),"a"(__in2));
    这个语句中,%0对应Output操作表达式"=a"(__out),而"=a"(__out)指定的寄存器是%eax,所以,占位符%0被替换为%eax;占位符%1对应Input操作表达式"m"(__in1),而"m"(__in1)被指定为内存,所以,占位符%1被替换位__in1的内存地址;
    用一句话描述:序号占位符就是前面描述的%0、%1、%2、%3、%4、%5、%6、%7、%8、%9;其中,每一个占位符对应一个Input/Output的C/C++表达式;

完成

分析

inline jlong Atomic::cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value) {
bool mp = os::is_MP();
__asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"   //占位符, cmpxchgq exchange_value 第2二表达式 ,(dest)第四个表达式
: "=a" (exchange_value)//输出约束,:movl %eax,address_of_exchange_value;
: "r" (exchange_value), //将(内的值)放入通用寄存器     "a" (compare_value), //将(括号内的值)放入rax寄存器    "r" (dest), // 将(dest)放入通用寄存器   "r" (mp) //放入通用寄存器
: "cc", "memory");
return exchange_value;  返回
}

2.2 cmpxchgq
cmpxchg是汇编指令
作用:比较并交换操作数.
如:CMPXCHG r/m,r 将累加器AL/AX/EAX/RAX中的值与首操作数(目的操作数)比较,如果相等,第2操作数(源操作数)的值装载到首操作数,zf置1。如果不等,首操作数的值装载到AL/AX/EAX/RAX并将zf清0

一般cmpxchg有两种存在形式
cmpxchg bx, cx:
如果AX与BX相等,则CX送BX且ZF置1;否则BX送AX,且ZF清0
换成AT&T的格式就是
由于ATT汇编与Intel不同,操作数2,和操作数1互换。
cmpxchg %cx, %bx

那么分析 cmpxchgq exchange_value 第2二表达式 ,(dest)第四个表达式

  • 如果ax 与bx相等,则cx 双向交换 bx,
  • 否则,否则就单方面地将 dest 指向的内存值交给ax
    • 第一步,将参数1的值放入到rax中,
    • 如果cas更新成功,如果相等,则双向交换 dest 与 exchange_value  返回rax,此时rax为参数1
    • 如果cas失败,否则就单方面地将 dest 指向的内存值交给 ``exchange_value,返回rax,此时rax为参数3

那么本条就是

========================================================================================================================

第一步:rax中的值设置为exchange_value  

第二步:判断rax中的值比较 dest 指针指向的内存值是否相等,如果相等,则双向交换dest 与 exchange_value,

    此时的dest指针中的内存值为新值,  "cmpxchgq %1,(%3)" ,那么 %1中的值就变为了原始内存值(亦为预期值),这个%1寄存器也是%rax寄存器,

    所以此时成功后rax寄存器中的值为参数3

第三步:如果二者不相等,那么将

 dest 指向的内存值交给 ``exchange_value,即将参数2的值(bx)的值给rax,那么此时rax的值为参数2

===========================================================================================================================

如果 ax 与 bx(dest)相等,则cx(exchange_value )与bx进行双向交互,那么此时ax中为exchange_value 即期望更新的值

否则 bx(dest)送ax

整个程序返回rax

结合实际分析一下源码

jvm源码解读--16 cas 用法解析

将要执行cas操作,

第一步,将rax中的值设置为Self

预期内存地址中的值为0x0,如果成功_owener的值为Self, %1 (%3)交换的第一部分,将%1(rax)的值变为0x0,所以返回值为0x0,接下来判断cur==NULL代表cas成功

如果失败,则将owner内存值给rax,那么rax中就是其他线程的值

那么此时