找回密码
 立即注册
首页 业界区 业界 Java的volatile和sychronized底层实现

Java的volatile和sychronized底层实现

嗦或 5 天前
1. 概览

从Java代码级别到硬件级别各层都是如何实现的
1.png

2. Synchronized

2.1 字节码层面

使用javap -verbose 可以查看到字节码信息,其中synchronized方法会有flags:ACC_SYNCHRONIZED,此时字节码中不会包含monitorenter和moniotrexit,JVM会自动加
  1. public synchronized void syncMethod();
  2.   flags: ACC_PUBLIC, ACC_SYNCHRONIZED
复制代码
使用``javap -verbose `编译一个带synchronized块的代码可以看到字节码中的monitorenter和moniotrexit
  1. 0: new #2                  // 创建一个新的Object实例
  2. 3: dup
  3. 4: invokespecial #1        // 调用Object的构造函数
  4. 7: astore_1                // 将引用存储到局部变量1(lock)
  5. 8: aload_1                 // 将局部变量1(lock)加载到操作数栈
  6. 9: monitorenter            // 进入monitor
  7. 10: ...                    // 同步块体的字节码
  8.    : aload_1
  9.    : monitorexit           // 退出monitor
  10.    : ...
复制代码
2.2 JVM层面

源码可以在Github上面查看
monitorenter底层是由JVM的代码ObjectMonitor来实现的
  1. ObjectMonitor() {
  2.    // 多线程竞争锁进入时的单向链表
  3.    ObjectWaiter * volatile _cxq;
  4.    //处于等待锁block状态的线程,会被加入到该列表
  5.    ObjectWaiter * volatile _EntryList;
  6.    // _header是一个markOop类型,markOop就是对象头中的Mark Word
  7.    volatile markOop _header;
  8.    // 抢占该锁的线程数,约等于WaitSet.size + EntryList.size
  9.    volatile intptr_t _count;
  10.    // 等待线程数
  11.     volatile intptr_t _waiters;
  12.    // 锁的重入次数
  13.    volatile intptr_ _recursions;
  14.    // 监视器锁寄生的对象,锁是寄托存储于对象中
  15.    void* volatile  _object;
  16.    // 指向持有ObjectMonitor对象的线程
  17.    void* volatile _owner;
  18.    // 处于wait状态的线程,会被加入到_WaitSet
  19.    ObjectWaiter * volatile _WaitSet;
  20.    // 操作WaitSet链表的锁
  21.    volatile int _WaitSetLock;
  22.    // 嵌套加锁次数,最外层锁的_recursions属性为0
  23.    volatile intptr_t  _recursions;
  24. }
复制代码
2.2.1 enter方法

整个方法比较长,但我们了解的无锁、偏向锁、轻量级锁、重量级锁都可以看到,核心方法是Atomic::cmpxchg_ptr,这个是CAS操作
锁方法描述偏向锁Atomic::cmpxchg_ptr将owner替换为当前线程,成功则获取到锁轻量级锁TrySpin->Atomic::cmpxchg_ptr不断自旋将owner替换为当前线程,成功则获取到锁重量级锁EnterI>Atomic::cmpxchg_ptrpark然后将owner替换为当前线程,成功则获取到锁
  1. void ATTR ObjectMonitor::enter(TRAPS) {
  2.   // The following code is ordered to check the most common cases first
  3.   // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  4.   Thread * const Self = THREAD ;
  5.   void * cur ;
  6.   
  7.   // 无锁CAS 转为 偏向锁
  8.   cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  9.   if (cur == NULL) {
  10.      // Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
  11.      assert (_recursions == 0   , "invariant") ;
  12.      assert (_owner      == Self, "invariant") ;
  13.      // CONSIDER: set or assert OwnerIsThread == 1
  14.      return ;
  15.   }
  16.        
  17.         // 可重入锁
  18.   if (cur == Self) {
  19.      // TODO-FIXME: check for integer overflow!  BUGID 6557169.
  20.      _recursions ++ ;
  21.      return ;
  22.   }
  23.        
  24.   if (Self->is_lock_owned ((address)cur)) {
  25.     assert (_recursions == 0, "internal state error");
  26.     _recursions = 1 ;
  27.     // Commute owner from a thread-specific on-stack BasicLockObject address to
  28.     // a full-fledged "Thread *".
  29.     _owner = Self ;
  30.     OwnerIsThread = 1 ;
  31.     return ;
  32.   }
  33.   // We've encountered genuine contention.
  34.   assert (Self->_Stalled == 0, "invariant") ;
  35.   Self->_Stalled = intptr_t(this) ;
  36.   // Try one round of spinning *before* enqueueing Self
  37.   // and before going through the awkward and expensive state
  38.   // transitions.  The following spin is strictly optional ...
  39.   // Note that if we acquire the monitor from an initial spin
  40.   // we forgo posting JVMTI events and firing DTRACE probes.
  41.   // 自旋获取锁
  42.   if (Knob_SpinEarly && TrySpin (Self) > 0) {
  43.      assert (_owner == Self      , "invariant") ;
  44.      assert (_recursions == 0    , "invariant") ;
  45.      assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ;
  46.      Self->_Stalled = 0 ;
  47.      return ;
  48.   }
  49.   assert (_owner != Self          , "invariant") ;
  50.   assert (_succ  != Self          , "invariant") ;
  51.   assert (Self->is_Java_thread()  , "invariant") ;
  52.   JavaThread * jt = (JavaThread *) Self ;
  53.   assert (!SafepointSynchronize::is_at_safepoint(), "invariant") ;
  54.   assert (jt->thread_state() != _thread_blocked   , "invariant") ;
  55.   assert (this->object() != NULL  , "invariant") ;
  56.   assert (_count >= 0, "invariant") ;
  57.   // Prevent deflation at STW-time.  See deflate_idle_monitors() and is_busy().
  58.   // Ensure the object-monitor relationship remains stable while there's contention.
  59.   Atomic::inc_ptr(&_count);
  60.   EventJavaMonitorEnter event;
  61.   { // Change java thread status to indicate blocked on monitor enter.
  62.     JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
  63.     DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
  64.     if (JvmtiExport::should_post_monitor_contended_enter()) {
  65.       JvmtiExport::post_monitor_contended_enter(jt, this);
  66.     }
  67.     OSThreadContendState osts(Self->osthread());
  68.     ThreadBlockInVM tbivm(jt);
  69.     Self->set_current_pending_monitor(this);
  70.     // TODO-FIXME: change the following for(;;) loop to straight-line code.
  71.     for (;;) {
  72.       jt->set_suspend_equivalent();
  73.       // cleared by handle_special_suspend_equivalent_condition()
  74.       // or java_suspend_self()
  75.                        
  76.                         // 重量级锁
  77.       EnterI (THREAD) ;
  78.     省略.......
  79. }
复制代码
2.2.2 cmpxchg_ptr

上面的锁都用了这个方法cmpxchg_ptr,这个和java中的cas是类似的,那它又是怎么实现的呢
atomic源码
其中cmpxchg是Linux操作系统的函数,执行了一段汇编指令,并且有lock前缀
  1. // 多核心多cpu前面就要加lock
  2. #define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "
  3. inline intptr_t Atomic::cmpxchg_ptr(intptr_t exchange_value, volatile intptr_t* dest, intptr_t compare_value) {
  4.   return (intptr_t)cmpxchg((jlong)exchange_value, (volatile jlong*)dest, (jlong)compare_value);
  5. }
  6. inline jlong    Atomic::cmpxchg    (jlong    exchange_value, volatile jlong*    dest, jlong    compare_value) {
  7.   bool mp = os::is_MP();
  8.   __asm__ __volatile__ (LOCK_IF_MP(%4) "cmpxchgq %1,(%3)"
  9.                         : "=a" (exchange_value)
  10.                         : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
  11.                         : "cc", "memory");
  12.   return exchange_value;
  13. }
复制代码
3. Volatile

3.1 字节码层面
  1.   static volatile int greaterThanSevenCnt;
  2.     descriptor: I
  3.     flags: ACC_STATIC, ACC_VOLATILE
复制代码
3.2 JVM层面

Github源码
可以看到判断是否是volatile字段,是的话最后会有OrderAccess::storeload(); , 就是就是storeload屏障
  1. CASE(_putfield):
  2. CASE(_putstatic):
  3.     {
  4.           // .... 省略若干行
  5.           // ....
  6.           // Now store the result 现在要开始存储结果了
  7.           // ConstantPoolCacheEntry* cache;     -- cache是常量池缓存实例
  8.           // cache->is_volatile()               -- 判断是否有volatile访问标志修饰
  9.           int field_offset = cache->f2_as_index();
  10.           if (cache->is_volatile()) { // ****重点判断逻辑****
  11.             // volatile变量的赋值逻辑
  12.             if (tos_type == itos) {
  13.               obj->release_int_field_put(field_offset, STACK_INT(-1));
  14.             } else if (tos_type == atos) {// 对象类型赋值
  15.               VERIFY_OOP(STACK_OBJECT(-1));
  16.               obj->release_obj_field_put(field_offset, STACK_OBJECT(-1));
  17.               OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
  18.             } else if (tos_type == btos) {// byte类型赋值
  19.               obj->release_byte_field_put(field_offset, STACK_INT(-1));
  20.             } else if (tos_type == ltos) {// long类型赋值
  21.               obj->release_long_field_put(field_offset, STACK_LONG(-1));
  22.             } else if (tos_type == ctos) {// char类型赋值
  23.               obj->release_char_field_put(field_offset, STACK_INT(-1));
  24.             } else if (tos_type == stos) {// short类型赋值
  25.               obj->release_short_field_put(field_offset, STACK_INT(-1));
  26.             } else if (tos_type == ftos) {// float类型赋值
  27.               obj->release_float_field_put(field_offset, STACK_FLOAT(-1));
  28.             } else {// double类型赋值
  29.               obj->release_double_field_put(field_offset, STACK_DOUBLE(-1));
  30.             }
  31.             // *** 写完值后的storeload屏障 ***
  32.             OrderAccess::storeload();
  33.           } else {
  34.             // 非volatile变量的赋值逻辑
  35.             if (tos_type == itos) {
  36.               obj->int_field_put(field_offset, STACK_INT(-1));
  37.             } else if (tos_type == atos) {
  38.               VERIFY_OOP(STACK_OBJECT(-1));
  39.               obj->obj_field_put(field_offset, STACK_OBJECT(-1));
  40.               OrderAccess::release_store(&BYTE_MAP_BASE[(uintptr_t)obj >> CardTableModRefBS::card_shift], 0);
  41.             } else if (tos_type == btos) {
  42.               obj->byte_field_put(field_offset, STACK_INT(-1));
  43.             } else if (tos_type == ltos) {
  44.               obj->long_field_put(field_offset, STACK_LONG(-1));
  45.             } else if (tos_type == ctos) {
  46.               obj->char_field_put(field_offset, STACK_INT(-1));
  47.             } else if (tos_type == stos) {
  48.               obj->short_field_put(field_offset, STACK_INT(-1));
  49.             } else if (tos_type == ftos) {
  50.               obj->float_field_put(field_offset, STACK_FLOAT(-1));
  51.             } else {
  52.               obj->double_field_put(field_offset, STACK_DOUBLE(-1));
  53.             }
  54.           }
  55.           UPDATE_PC_AND_TOS_AND_CONTINUE(3, count);
  56.   }
复制代码
进入OrderAccess源码可以看到,直接执行了一段汇编指令,并且有lock前缀
  1. inline void OrderAccess::storeload()  { fence(); }
  2. inline void OrderAccess::fence() {
  3.   if (os::is_MP()) {
  4.     // always use locked addl since mfence is sometimes expensive
  5. #ifdef AMD64
  6.     __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
  7. #else
  8.     __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
  9. #endif
  10.   }
  11. }
复制代码
4. lock指令

在上面的分析中,最底层都设计到汇编层面的lock指令,这个指令有什么作用呢?
根据汇编参考文档IA-32 Assembly Language Reference Manual
The LOCK # signal is asserted during execution of the instruction following the lock prefix. This signal can be used in a multiprocessor system to ensure exclusive use of shared memory while LOCK # is asserted. The bts instruction is the read-modify-write sequence used to implement test-and-run. The lock prefix works only with the instructions listed here. If a lock prefix is used with any other instructions, an undefined opcode trap is generated.
Lock是一个指令前缀,用于多核处理器系统不使用共享内存
那么它又是怎么让其他核心不访问共享内存,有两种方法

  • 锁内存总线,也就是说执行这条指令的时候,其他的核心都不能在访问内存了
  • 锁缓存行,现在CPU本身是有多级缓存的,而这些缓存是如何保持一致的,由MESI来支持,MESI协议可以保证其他核心不使用内存,或者换一种说法,可以使用,但被修改的内容会失效
5. MESI协议

现代CPU多核架构中为了协调快速的CPU运算和相对较慢的内存读写速度之间的矛盾,在CPU和内存之间引入了CPU cache:
2.png

MESI协议下,缓存行(cache line)有四种状态来保证缓存的一致性

  • 已修改Modified (M) 缓存行是脏的,与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S)
  • 独占Exclusive (E) 缓存行只在当前缓存中,但是干净的(clean)–缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。
  • 共享Shared (S) 缓存行也存在于其它缓存中且是干净的。缓存行可以在任意时刻抛弃。
  • 无效Invalid (I) 缓存行是无效的,需要从主内存中读取最新值
每次要修改缓存,如果缓存行状态为 S 的话都要先发一个 invalidate 的广播,再等其他 CPU 将缓存行设置为无效后返回 invalidate ack 才能写到 Cache 中,因为这样才能保证缓存的一致性
但是如果 CPU 频繁地修改数据,就会不断地发送广播消息,CPU 只能被动同步地等待其他 CPU 的消息,显然会对执行效率产生影响
为了解决此问题,工程师在 CPU 和 cache 之间又加了一个 store buffer,同时在cache和总线之间添加了Invalidate Queue
这个buffer可以让广播和收广播的处理异步化,效率当然会变高,但强一致性变为了最终一致性
lock指令是CPU硬件工程师给程序员留的一个口子,把对MESI协议的优化(store buffer, invalidate queue)禁用,暂时以同步方式工作,使得对于该关键字的MESI协议退回强一致性状态
6. 总结

分析到此:
所有的并发问题可以概括为,多个核心同时修改内存数据,导致结果不符合预期
解决并发问题的方法可以概括为,同一时间只能让一个核心修改内存,但有多种手段,例如锁总线、或者广播让其他核心失效
7. 其他问题


  • 既然sychronized的和volatile底层实现是一样的,那么volatile为什么没有原子性呢?
    在于锁定的范围,volatile修饰的是一个字段,只能保证读和写是原子性的,但读出来、在计算、写入分为三步则不是原子性的。
    sychronized底层也用了volatile的,但它的锁定范围是程序员指定的,这个范围之间的代码是原子的
    1. cas volatile变量开始锁定
    2. 任意程序代码
    3. cas volatile变量释放锁定
    复制代码
  • 现在一般推荐使用Java的Atomic类,他是通过CAS来实现的,它和sychronized的区别是什么?
    cas不能单独使用,需要加自旋操作,本身是一个乐观锁
    sychronized本身结合了乐观锁和悲观锁,悲观锁会让线程park然后重试,不会消耗CPU,而乐观锁但不断消耗cpu
8. 对比

在阅读ObjectMonitor代码时,发现有很熟悉的感觉
3.png

发现这些锁的数据结果都是类似的,一个volatile变量加一个等待队列
参考

【1】]synchronized 关键字底层原理
【2】Java多线程:objectMonitor源码解读(3)
【3】Linux Kernel CMPXCHG函数分析
【4】聊聊CPU的LOCK指令
【5】12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?
【6】MESI和volatile的关系详解
【7】volatile底层原理详解
【8】浅析mutex实现原理
【9】CAS你以为你真的懂?
【10】x86 LOCK 指令前缀
【11】Linux Mutex机制分析

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册