Synchronize底层实现原理

本文详细介绍了Java中synchronized关键字的三种使用方式及其底层实现原理,包括实例方法、静态方法和代码块的同步。通过分析字节码和对象头的MarkWord,揭示了锁的状态变化,涉及无锁、偏向锁、轻量级锁和重量级锁。此外,还讨论了JDK1.6后的锁优化,如自旋锁和自适应自旋锁。

转载文章:https://www.cnblogs.com/semi-sub/p/12906660.html

相信对Java程序员来说,synchronized关键字对大家来说并不陌生,当我们遇到并发情况时,优先会想到用synchronized关键字去解决,synchronized确实能够帮助我们去解决并发的问题,但是它会引起一些其他问题,比如最突出的一点就是程序效率问题,不过后面随着JDK1.6对synchronized关键字做出了许多优化,让synchronizedjava.util.concurrent.locks.ReentrantLock等并发包中的类效率差不多。下面主要来分析一下synchronized底层的实现原理。

1 基本使用

   synchronized关键字可以用来修饰三个地方:

1.synchronized放在实例方法上,锁对象是当前的this对象

2.synchronized放在类方法上,也就是我们所说的静态方法上,锁对象是方法区中的类对象,是一个全局锁

3.synchronized修饰代码块,也就是synchronized(object){},锁对象是()中的对象

synchronized关键字用来修饰的位置不同,其实现原理也是不同的。锁住的对象也是不同的。

在Java中,每个对象里面隐式的存在一个叫monitor(对象监视器)的对象,这个对象源码是采用C++实现的,下面来看一下Monitor对象的源码:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; // 记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

当monitor对象被线程持有时,Monitor对象中的count就会进行+1,当线程释放monitor对象时,count又会进行-1操作。用count来表示monitor对象是否被持有.

2 同步原理

    针对synchronized修饰的地方不同,实现的原理不同

1.先看synchronized放在实例方法上的代码:

public class Test {

  public synchronized void test() {
  }
}

用javap -verbose查看反编译结果:

从反编译的结果来看,我们可以看到test()方法中多了一个标识符。JVM就是根据该ACC_SYNCHRONIZED标识符来实现方法的同步:

当方法被执行时,JVM调用指令会去检查方法上是否设置了ACC_SYNCHRONIZED标识符,如果设置了ACC_SYNCHRONIZED标识符,则会获取锁对象的monitor对象,线程执行完方法体后,又会释放锁对象的monitor对象。在此期间,其他线程无法获得锁对象的monitor对象

 

2.第二种情况,synchronized放在类方法上:

public class Test {

    public synchronized static void test(){
    }
}

反编译结果:

我们可以看到跟放在实例方法相同,也是test()方法上会多一个标识符。可以得出synchronized放在实例方法上和放在类方法上的实现原理相同,都是ACC_SYNCHRONIZED标识符去实现的。只是它们锁住的对象不同

3.第三种情况,synchronized修饰代码块:

public class Test {

    public void test(){
        synchronized (this) {
        }
    }
}

反编译结果:

我们可以看到test()字节码指令中会有两个monitorenter和monitorexit指令,

(1)monitorenter: monitorenter指令表示获取锁对象的monitor对象,这是monitor对象中的count并会加+1,如果monitor已经被其他线程所获取,该线程会被阻塞住,直到count=0,再重新尝试获取monitor对象

(2)monitorexit: monitorexit与monitorenter是相对的指令,表示进入和退出。执行monitorexit指令表示该线程释放锁对象的monitor对象,这时monitor对象的count便会-1变成0,其他被阻塞的线程可以重新尝试获取锁对象的monitor对象

 

synchronized放置的位置不同可以得出,synchronized用来修饰方法时,是通过ACC_SYNCHRONIZED标识符来保持线程同步的。而用来修饰代码块时,是通过monitorentermonitorexit指令来完成

3 同步概念

3.1 Java对象头

    像我们上面所提到的monitor监视器对象存在于Java对象的对象头Mark Word中,

什么是Java对象头的Mark Word了?这就跟Java对象在JVM中的内存布局有关系,Java对象在JVM内存中分为三块区域:对象头,实例数据和对齐填充

1. 对象头:对象头又包含以下部分:Mark Word, 类型指针,如果对象是数组的话,还会存在一个数据数组长度,用来记录数据长度。其中Mark Word又包含:哈希码(HashCode),GC分代年龄,锁状态标志,线程持有的锁等信息。
2. 实例数据:这部分是对象真正存储的有效信息,也就是我们在程序中所定义的各种类型的字段内容。
3. 对齐填充:这部分数据并不是必然存在的,JVM内存管理系统要求我们定义的对象大小必须是8字节的整数倍,如果对象大小不是整数倍,这时就会有对齐填充这块数据,来将对象大小补全成为8字节的整数倍

如图所示:

下面结合锁的优化,来讲一下Mark Word中的锁状态发生的变化:

  1. 无锁

    也就是代表对象的monitor对象并没有被线程所持有,代表的是对象处于无锁状态

  2. 偏向锁

    偏向锁是JDK1.6后面引起的一项锁优化技术,在无锁竞争的情况下,一个线程通过一次CAS操作来尝试将对象头中的Thread ID字段设置为自己的线程号,如果设置成功,则获得锁,那么以后线程再次进入和退出同步块时,就不需要使用CAS来获取锁,只是简单的测试一个对象头中的Mark Word字段中是否存储 着指向当前线程的偏向锁。如果使用CAS设置失败时,说明存在锁的竞争,这时偏向锁便会升级成轻量级锁和重量级锁。偏向锁指的是这个锁会偏向于第一个获得它的线程。

    下面来看一下对象在无锁和偏向锁状态下,Mark Word的锁状态位变化:

    我们可以看到这是偏向锁的标志位被至为了1,表示现在处于偏向模式

  3. 轻量级锁

    ​ 表示线程通过一定的数量CAS操作(JDK1.6后默认10次)完成加锁和解锁操作,如果锁获取失败,会通过自旋来获取,竞争的线程不会阻塞,如果还是获取失败,表示此时存在其他线程竞争锁(两条或两条以上的线程竞争同一个锁),则轻量级锁会膨胀成重量级锁。
  4. 重量级锁

    ​ 当一个锁被两条或两条以上的线程竞争的时候,这时候轻量级锁就会演变成重量级锁。
    在轻量级锁和重量级锁的情况下,锁状态标记位:

    除了以上几种锁外,其实还有自旋锁,自适应自旋锁
  5. 自旋锁
    ​ 当两个线程去竞争同一把锁时,一个线程获取成功,一个线程获取失败,这时可能会出现获取成功的线程持有锁的时间非常短,如果这时候将获取失败的线程进行挂起的话,会造成功线程上下文的切换,到时候又需要唤醒线程,这时我们可以让获取失败的线程进行一个自旋,无需将线程挂起。等到锁释放。但是这种方案适用于锁被占用的时间很短的情况,如果锁被持有的时间很长,然后线程将会一直处于自旋状态,白白消耗处理器资源。自旋等待的时间必须要有一定的限度,如果超过此数还是没有获取到,则将线程挂起。自旋此数的默认值是10次,可以使用JVM参数-XX:PreBlockSpin来进行更改。

  6. 自适应自旋锁

自适应自旋锁是在自旋锁的基础上产生的,进行了一次优化。自适应意味着自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间几锁的拥有者的状态来决定的。

锁的优缺点对比

结论:

    从上面分析得出:synchronized关键字底层实现主要是通过monitor对象去完成的,修饰的位置不同,实现的方式有点差异,但是底层都是通过monitor对象去实现的。后面还讲了JDK1.6对synchronized关键字做出的锁优化。

转载至:https://www.cnblogs.com/semi-sub/p/12906660.html

<think>好的,用户之前让我介绍了Linux内核跟踪点(Tracepoint),现在他们进一步询问底层原理。我需要深入解释Tracepoint是如何在内核中工作的,包括它的代码结构、触发机制、数据收集方式以及性能优化等方面。 首先,用户可能已经了解Tracepoint的基本概念和使用方法,现在需要更技术性的细节。他们可能是一位开发者或系统管理员,希望深入理解以便调试或优化内核性能。所以,我需要确保回答结构清晰,逐步深入,同时保持专业性但不至于过于晦涩。 接下来,我应该从Tracepoint的代码结构开始,因为用户之前提到了TRACE_EVENT宏。这部分需要详细说明宏如何展开,各个部分的作用,比如TP_PROTO定义参数,TP_ARGS传递参数,TP_STRUCT__entry定义数据结构,TP_fast_assign进行数据记录,以及TP_printk格式化输出。这些内容能帮助用户理解如何自定义Tracepoint。 然后,触发机制部分需要解释条件跳转和rcu机制。用户可能对为什么Tracepoint在未启用时开销低感兴趣,所以需要说明跳转指令和分支预测的影响,以及启用后的处理流程,比如调用函数和RCU同步的重要性。 数据采集方面,用户可能想知道数据是如何从内核传递到用户空间的,这里要提到ring buffer和内存屏障,确保数据一致性和高效传输。同时,提到perf和ftrace等工具如何利用这些机制,可以帮助用户联系实际应用。 性能优化是关键点,用户可能关心为什么Tracepoint比其他工具(如kprobe)更高效,需要比较静态代码与动态插桩的区别,内存预分配减少碎片,以及RCU锁减少竞争。这些细节能帮助用户理解Tracepoint的优势。 最后,与kprobe的对比和实际应用场景,可以巩固用户的理解,展示Tracepoint的适用场景和限制。比如,静态存在的Tracepoint稳定性高,但需要内核支持,而kprobe更灵活但可能影响稳定性。 需要确保每个部分都有技术细节,但不过于冗长,使用示例代码和结构图帮助说明。同时,避免使用过多术语而不解释,保持语言简洁明了。检查是否有遗漏的关键点,比如Tracepoint如何与eBPF结合使用,这在之前的回答中提到了,可以简要带过。 现在,整理这些思路,分步骤详细解释底层原理,确保覆盖代码结构、触发机制、数据采集、性能优化和对比分析,最后总结应用场景,帮助用户全面理解Tracepoint的底层工作原理。</think>以下是Linux内核跟踪点(Tracepoint)底层原理的逐层解析: $$Tracepoint_{原理} = 静态代码桩 + 条件跳转 + 动态回调注册$$ 一、代码结构层 1. 宏定义机制:通过`TRACE_EVENT`宏展开为多层结构 ```c // 展开示例(简化版): TRACE_EVENT(name, proto, args, struct, assign, print) ↓ 生成 ↓ struct trace_event_call event_##name; // 事件描述结构体 trace_event_type_funcs_##name; // 类型处理函数 trace_event_define_fields_##name(); // 字段定义 ``` 2. 桩函数实现: ```c // 每个跟踪点生成桩函数 static inline void trace_##name(proto) { if (static_key_false(&__tracepoint_##name.key)) // 快速条件检查 __DO_TRACE(&__tracepoint_##name, args); // 实际调用 } ``` 二、触发机制 1. 条件跳转优化:使用`static_key`机制实现近乎零开销判断 - 未启用时:仅执行1个`nop`指令(5字节空间) - 启用时:替换为`jmp`指令跳转到处理程序 2. RCU同步机制: ```c // 回调函数注册过程 rcu_assign_pointer(tp->funcs, new_funcs); // 原子指针替换 synchronize_rcu(); // 等待所有CPU退出临界区 ``` 三、数据采集流程 1. 内存布局优化: ```text +-------------------+ +-----------------+ | 跟踪点调用 | →→ | 环形缓冲区 | | (trace_##name) | | (per-cpu) | +-------------------+ +-----------------+ ↓ ↓ 事件过滤层 内存屏障保证数据一致性 ↓ ↓ 格式处理层 用户空间映射区(mmapped) ``` 2. 数据序列化过程: ```text struct trace_entry { unsigned short type; // 事件类型ID unsigned char flags; // 状态标志 unsigned char preempt_count; // 抢占计数 int pid; // 进程ID }; __entry动态数据区 → 根据TP_STRUCT定义生成二进制紧凑结构 四、性能优化设计 1. 分支预测优化: ```asm ; x86架构实现示例 trace_syscall_entry: jmp 1f ; 5字节跳转指令 nop ; 填充对齐 1: ...实际处理代码... ``` 2. 内存预分配策略: - 每个CPU核心独立环形缓冲区(避免锁竞争) - 事件结构体按最大尺寸预分配(消除内存碎片) 3. 无锁设计: ```c // 写缓冲区使用本地变量 struct ring_buffer_event *event; event = ring_buffer_lock_reserve(buffer, len); memcpy(event->data, &entry, len); ring_buffer_unlock_commit(buffer, event); ``` 五、与kprobe的底层差异对比 ```c // Tracepoint (静态桩) void trace_syscall_entry() { if (unlikely(tp_active)) // 单次条件判断 do_trace(); // 直接调用处理函数 } // kprobe (动态插桩) kprobe: int3陷阱 → 保存寄存器 → 调用处理程序 → 恢复执行 ``` 六、实际执行时序 1. 内核线程触发事件 2. 通过static_key判断是否激活 3. 获取RCU读锁 4. 分配环形缓冲区空间 5. 序列化事件数据 6. 提交到缓冲区 7. 唤醒消费者进程 关键路径耗时分析: ```text +---------------------+------------+ | 操作步骤 | 时钟周期估算 | +---------------------+------------+ | 条件判断 | 1-3 cycle | | 内存分配 | 50-100 | | 数据复制 | 10-30/byte | | 屏障操作 | 20-40 | | 上下文切换(无消费者) | 0 | +---------------------+------------+ ``` 该设计通过以下方式保证高效性: 1. 冷路径隔离:未启用时只有单个条件判断 2. 无阻塞写入:环形缓冲区实现生产-消费解耦 3. 内存预取优化:数据结构缓存对齐(CACHELINE_SIZE) 4. 编译时优化:通过__always_inline避免函数调用开销 应用场景示例: ```text 当跟踪sys_write系统调用时: 1. 用户echo 1 > enable触发桩函数激活 2. 每个sys_write调用执行jmp到处理程序 3. 收集参数(fd, buf, count)到缓冲区 4. 用户空间工具解析二进制数据 5. 生成可读日志(如fd=3, count=1024) ``` 通过这种底层设计,Tracepoint在保持观测能力的同时,将运行时开销控制在可接受范围内(通常<5%性能损耗),成为生产环境可用的内核级诊断工具。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值