面试16解析-深挖锁(上)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 题目:请分析一下Java锁机制的实现原理(主要是画Monitor示意图);Java锁有哪些种类,以及它们的区别?

本文阅读大概需要25分钟。


这个题目主要考查锁的原理及种类。由于这个题目涉及的内容比较多,分为多篇来解答。



一什么是锁(Lock)?


这里我们先来说说什么是锁?在计算机科学设计的模型中,很多模型都来自于现实生活,锁便是其中一例。在现实生活中,为了保护我们的房间不被其他人随意进入,可以给房门上把锁,只有获取了该锁钥匙的人,才能打开锁,进入房间。而在软件开发中,也正借鉴了现实生活中的锁的功能与用途,抽象出了锁的概念,多线程就类比进入该房间的人,而被锁保护的代码就是房间,只有拥有钥匙(获取了锁)的人(线程)才能进入房间(被保护的同步块代码)。


二synchronized


Java中的关键字synchronized便是一种锁,只是synchronized会隐式的进行获取锁与释放锁的操作。下面我们通过一个例子来分析一下synchronized的使用:

static Object lock = new Object();
static int shareSafeCount = 0;
static int shareCount = 0;
static void synchronizedExample() throws Exception {
    for (int ix = 0; ix != 2000; ix++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                shareCount++;
                synchronized (lock) {
                    shareSafeCount++;
                }
            }
        }).start();
    }
    Thread.sleep(10000); //sleep2秒,等待线程执行完成(实际代码中不推荐这样等待线程完成)
    System.out.println("shareCount:" + shareCount);
    System.out.println("shareSafeCount:" + shareSafeCount);
}


/

作者本机的执行结果:

shareCount:1988
shareSafeCount:2000

在该示例中,创建了2000个工作线程对两个共享变量进行自增操作,其中对shareSafeCount的累加操作是线程安全的,被synchronized同步块保护,其运行结果不出所料是2000,而线程对于shareCount变量的操作却由于没有被互斥保护,其运行结果不一定是2000(注意是不一定!)。有很多同学在编写这样的例子中,经常会发现两个变量的运行结果都是2000,其原因在于自增操作(同步块内的操作)运行速度很快,其运行速度快于线程创建的速度,因此看似创建了2000个线程,实则只是依次创建了2000个线程对一个变量进行累加而已,并没有遇到多线程对同一资源竞争的情况,这也是本作者在run()函数中让线程休眠100ms的原因,目的在于不让线程退出的那么快,对变量进行资源竞争。


好了,我们回归正题,来看看jvm是如何保证synchronized包裹的代码线程安全的。我们看看这段代码对应的jvm指令是如何的(这里只截取了关键jvm指令部分):


10: getstatic     #6                  // Field MutiThreadTest.shareCount:I
13: iconst_1
14: iadd
15: putstatic     #6             // Field MutiThreadTest.shareCount:I
18: getstatic     #7             // Field MutiThreadTest.lock:Ljava/lang/Object;
21: dup
22: astore_1
23: monitorenter
24: getstatic     #8             // Field MutiThreadTest.shareSafeCount:I
27: iconst_1
28: iadd
29: putstatic     #8             // Field MutiThreadTest.shareSafeCount:I
32: aload_1
33: monitorexit
34: goto          42


其中指令10到22是对shareCount的自增操作,指令23到33是对shareSafeCount的自增操作,通过对比可以发现,对shareSafeCount的操作多了两条指令moniterenter和moniterexit。这两条指令便是synchronized关键字的隐式获取锁与释放锁对应的指令,这两个指令划分了一片同步块,具有排他性,当有线程进入该同步块后,其他线程必须等待在monitereneter指令上,直到进入同步块的线程通过moniterexit指令退出后,其他线程才可以进入同步块。


从本质上来说,moniterenter与moniterexit是一组排他的对某一对象监视器进行尝试获取的过程(该对象正是示例中的lock),同一时刻只有一个线程成功获取对象监视器。在线程运行到同步块时,会通过moniterenter指令尝试获取对象的监视器,如果获取成功,则进入同步块,执行同步块内指令,如果获取失败,则会进入同步队列(SynchronizedQueue)中进行等待,线程当前状态变为BLOCK(阻塞)状态,直到有线程释放监视器。下图描述了moniterenter与moniterexit与线程运行的关系:

image.png


三深挖对象监控器Moniter


上文的分析过程,我们理解了sychronized同步代码块的同步原理是jvm提供的对象监控器获取机制来进行同步控制的,这里我们再来深挖一下对象监控器获取的实现,这一实现当然是在java虚拟机中实现的,下面是OpenJDK8中的Hotspot虚拟机源码(ObjectMoniter.cpp),对moniterenter的实现:


/

bool ObjectMonitor::try_enter(Thread* THREAD) {
  if (THREAD != _owner) {
    if (THREAD->is_lock_owned ((address)_owner)) {
       assert(_recursions == 0, "internal state error");
       _owner = THREAD ;
       _recursions = 1 ;
       OwnerIsThread = 1 ;
       return true;
    }
    if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
      return false;
    }
    return true;
  } else {
    _recursions++;
    return true;
  }
}

我们可以看到,运行线程获取到moniter的核心就是一个原子操作的Atomic::cmpxchg_ptr()是否成功(cmpxchg指令还熟悉吗?CAS的实现便是CMPXCHG),因此线程对moniter的获取操作同步控制的核心依然是依靠CAS操作的原子性来实现的。


四JVM的Atomic::cmpxchg_ptr()实现


这里我们对于同步的理解已经深挖如java虚拟机了,既然这样我们就深挖到底,看看java同步控制到底在cpu级别是如何保证的,这也是对之前一系列java同步文章的补充。下面是HotSpot虚拟机Atomic::cmpxchg_ptr()的在windows平台下的实现(作者比较熟悉windows系统编程,源码在atomic_windows_x86.inline.hpp中):


inline intptr_t Atomic::cmpxchg_ptr(intptr_t exchange_value, volatile intptr_t* dest, intptr_t compare_value) {
  return (intptr_t)cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}


/

这里还不是核心,继续看Atomic::cmpxchg()的实现:

inline jlong    Atomic::cmpxchg (jlong    exchange_value, volatile jlong*    dest, jlong    compare_value) {
  __asm {
    push ebx
    push edi
    mov eax, cmp_lo
    mov edx, cmp_hi
    mov edi, dest
    mov ebx, ex_lo
    mov ecx, ex_hi
    LOCK_IF_MP(mp)
    cmpxchg8b qword ptr [edi]
    pop edi
    pop ebx
  }
}

看到了吗?这段内联汇编代码便是CAS的同步操作的核心实现,在汇编指令CMPCHG指令之前,加入了lock前缀,如果是单处理器便会省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果),lock前缀在处理器级别确保了对内存的读-改-写操作原子执行。

相关文章
|
2天前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
13 1
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
334 37
|
14天前
|
NoSQL Java API
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴在面试一线互联网企业时遇到了关于Redis分布式锁过期及自动续期的问题。尼恩对此进行了系统化的梳理,介绍了两种核心解决方案:一是通过增加版本号实现乐观锁,二是利用watch dog自动续期机制。后者通过后台线程定期检查锁的状态并在必要时延长锁的过期时间,确保锁不会因超时而意外释放。尼恩还分享了详细的代码实现和原理分析,帮助读者深入理解并掌握这些技术点,以便在面试中自信应对相关问题。更多技术细节和面试准备资料可在尼恩的技术文章和《尼恩Java面试宝典》中获取。
美团面试:Redis锁如何续期?Redis锁超时,任务没完怎么办?
|
2月前
|
消息中间件 安全 前端开发
面试官:单核服务器可以不加锁吗?
面试官:单核服务器可以不加锁吗?
47 4
面试官:单核服务器可以不加锁吗?
|
26天前
|
存储 安全 Java
JVM锁的膨胀过程与锁内存变化解析
在Java虚拟机(JVM)中,锁机制是确保多线程环境下数据一致性和线程安全的重要手段。随着线程对共享资源的竞争程度不同,JVM中的锁会经历从低级到高级的膨胀过程,以适应不同的并发场景。本文将深入探讨JVM锁的膨胀过程,以及锁在内存中的变化。
24 1
|
2月前
|
存储 缓存 安全
【Java面试题汇总】多线程、JUC、锁篇(2023版)
线程和进程的区别、CAS的ABA问题、AQS、哪些地方使用了CAS、怎么保证线程安全、线程同步方式、synchronized的用法及原理、Lock、volatile、线程的六个状态、ThreadLocal、线程通信方式、创建方式、两种创建线程池的方法、线程池设置合适的线程数、线程安全的集合?ConcurrentHashMap、JUC
【Java面试题汇总】多线程、JUC、锁篇(2023版)
|
14天前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
2月前
|
缓存 Android开发 开发者
Android RecycleView 深度解析与面试题梳理
本文详细介绍了Android开发中高效且功能强大的`RecyclerView`,包括其架构概览、工作流程及滑动优化机制,并解析了常见的面试题。通过理解`RecyclerView`的核心组件及其优化技巧,帮助开发者提升应用性能并应对技术面试。
65 8
|
2月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
63 8
|
2月前
|
安全 Java 开发者
Java并发编程中的锁机制解析
本文深入探讨了Java中用于管理多线程同步的关键工具——锁机制。通过分析synchronized关键字和ReentrantLock类等核心概念,揭示了它们在构建线程安全应用中的重要性。同时,文章还讨论了锁机制的高级特性,如公平性、类锁和对象锁的区别,以及锁的优化技术如锁粗化和锁消除。此外,指出了在高并发环境下锁竞争可能导致的问题,并提出了减少锁持有时间和使用无锁编程等策略来优化性能的建议。最后,强调了理解和正确使用Java锁机制对于开发高效、可靠并发应用程序的重要性。
26 3

推荐镜像

更多