实战证明java中的两把锁ReentrantLock与synchronized的系统调用

本文从系统调用的角度深入探讨Java中ReentrantLock与synchronized的区别。尽管ReentrantLock通常被认为比synchronized轻量,但文章指出它们在底层都涉及系统调用。ReentrantLock的上锁通过CAS实现,而阻塞和唤醒线程则使用了pthread_cond_wait和pthread_cond_signal,这两个操作涉及sys_futex系统调用。相反,synchronized的锁获取和释放使用了CAS,阻塞和唤醒线程则依赖于Object.wait()和Object.notify(),底层同样涉及系统调用。文章强调不应简单地认为ReentrantLock就比synchronized轻,两者各有适用场景。
摘要由CSDN通过智能技术生成

时势造英雄:聪明和勤奋是成功的前置条件,世上聪明和勤奋的人多的去了,真正重要的是顺势而为!

背景

网上一大票文章都在说Java中的synchronized锁是重量级锁,因为使用了系统调用,会从用户态陷入内核态,开销很大, 性能影响大,而ReentrantLock使用的是CAS轻量级操作,性能开销小,虽然JDK1.6后对synchronized进行了锁升级的优化,但是还是避免不了人们synchronized性能比不上ReentrantLock的刻板映像!

究其原因就是synchronized很重!有系统调用,会从用户态陷入内核态,那ReentrantLock有没有系统调用呢?那么本文就从系统调用的角度分析一下两者

长文预警!!!

前置知识

ReentrantLock原理

一图胜前言,ReentrantLock从大局上看原理如下(注意ReentrantLock继承自AbstractQueuedSynchronizer)

image

  1. 一个数字state表示资源,一个线程尝试CAS地去+1,操作成功即上锁,那么可以欢快的执行锁内的代码
  2. 另一个哥们(线程)也来尝试CAS地+1,不好意思锁别人占着,你乖乖排队去(双向的CLH队列)阻塞,后面的线程来抢锁,抢不到都排队去就完事
  3. 第一个线程执行完释放锁资源,此时只有它自己在执行,欢快的将state置0(不用CAS),然后叫醒排在它后面的哥们(即队列中第二个节点)执行。

大体逻辑如上,其中涉及到很多对共享变量CAS自旋的细节操作,比如CAS入队、CAS操作state,不是文本重点,此处不细表

synchronized原理

synchronized由于是JDK自带的锁,是JVM层面去实现的(因为JDK1.6后synchronized有锁升级的过程,此处只分析synchronized重量级锁),具体是用ObjectMonitor来实现,开局一张原理图

Snipaste_2021-02-28_17-08-05.png

ObjectMonitor主要数据结构如下

 ObjectMonitor() {
    _count        = 0;   // 记录个数
    ...
    _owner        = NULL;//持有锁线程
    _WaitSet      = NULL;// 处于wait状态的线程,会被加入到_WaitSet
    ...
    _EntryList    = NULL;// 处于等待锁block状态的线程,会被加入到该列表
  }
  1. 想要获取monitor(即锁)的线程,首先会进入_EntryList队列。
  2. 当某个线程获取到对象的monitor后,进入_Owner区域,设置为当前线程,同时计数器_count+1
  3. 如果线程调用了Object#wait()方法,则会进入_WaitSet队列。它会释放monitor锁,即将_owner赋值为null,并且_count-1,进入_WaitSet队列阻塞等待。
  4. 如果其他线程调用 Object#notify() / notifyAll() ,会唤醒_WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入_Owner区域。
  5. 同步方法执行完毕了,线程退出临界区,会将monitor的_owner设为null,并释放监视锁。

系统调用

在电脑中,系统调用(英语:system call),指运行在用户空间的程序向操作系统内核请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态运行。如设备IO操作或者进程间通信。

说人话就是操作系统像一个黑盒子,运行在计算机硬件之上,你自己写的软件需要调用硬件的某些功能比如从磁盘打开一部电影,你的软件没法和硬盘直接交互的,必须告诉这个黑盒子,让黑盒子去硬盘里面去取,为啥要这样设计?

  • 安全性与稳定性:操作系统的东西你一个应用层软件不能乱碰,碰坏了宕机谁负责?这个能靠应用层软件自觉遵守?那肯定不行,否则就没有那么多病毒程序了,因此操作系统干脆直接不让你碰,只开放了安全的接口(系统调用)提供给你调用。
  • 屏蔽硬件的复杂性:硬件千奇百怪,各种型号,需要各种匹配的驱动才能运行,一个应用层软件想从硬盘读取数据,如果没有操作系统这个黑盒子给你提供便利(系统调用),那你要从硬盘驱动开始写?等你写好了塑料花儿都谢了。

所以,系统调用开销是很大的,因此在程序中尽量减少系统调用的次数,并且让每次系统调用完成尽可能多的工作,例如每次读写大量的数据而不是每次仅读写一个字符。

那么Linux有哪些系统调用?这里可以查(系统调用表):http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64

从系统调用的角度分析

就一个锁而言,那么关键的东西我认为是如何上锁以及如何让线程阻塞以及唤醒线程,那么就从这三个方面分析

ReentrantLock

如何上锁

所谓上锁在ReentrantLock就是给state变量+1,state声明如下,注意是volatile的,也就是在多线程环境下对每个线程都是可见的

private volatile int state;

那么很多线程都在抢这把锁,只有一个线程能抢到(即能执行state+1成功),怎么保证线程安全?答案是CAS,CAS是啥?简单来说就是Compare And Swap,即比较并替换:给一个预期值E和一个更新值U,如果当前值A和预期值E相等,则更新A为U。感觉是不是有点像乐观锁?

int c = getState();//c是state
if (c == 0) {//锁还没被别人抢
        if (compareAndSetState(0, acquires)) {//重点是这句,CAS方式设置state
        setExclusiveOwnerThread(current);
        return true;
    }
}

继续跟下去,调用了unsafe的compareAndSwapInt,在往下就是native方法了

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

/**
* 源码在http://hg.openjdk.java.net/jdk8/jdk8/jdk/file/687fd7c7986d/src/share/classes/sun/misc/Unsafe.java
* Unsafe.compareAndSwapInt
* Atomically update Java variable to x if it is currently
* holding expected
* @return true if successful
*/
public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);

继续跟踪在JVM中的实现,源码位置:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/tip/src/share/vm/prims/unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;//此处调用了Atomic::cmpxchg
UNSAFE_END

里面又调用了Atomic::cmpxchg,源码位置:http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
  • __asm__表示是汇编指令
  • LOCK_IF_MP,是否是多核处理器,如果是加上lock指令
  • lock 和cmpxchgl是CPU指令,lock指令是个前缀,可以修饰其他指令,cmpxchgl即为CAS指令

这个lock才是主角,它才是实现CAS的原子性的关键(因为现在基本都是多核处理器了,那么肯定会存在多个核心争抢资源的情况),在Intel® 64 and IA-32 Architectures Software Developer’s Manual 中的章节LOCK—Assert LOCK# Signal Prefix 中给出LOCK指令的详细解释

image

  • 总线锁

LOCK#信号就是我们经常说到的总线锁ÿ

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

代码狂魔v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值