精华内容
下载资源
问答
  • 对Java语言的每个语法都提供了一个或个例程讲解 大量使用流程图表示程序的执行过程,使用结构图表示程序的内部状态 每章最后都给出了典型的练习题,让读者及时练习,巩固提高,并提供了参考答案 目录 第1篇 ...
  • 对Java语言的每个语法都提供了一个或个例程讲解 大量使用流程图表示程序的执行过程,使用结构图表示程序的内部状态 每章最后都给出了典型的练习题,让读者及时练习,巩固提高,并提供了参考答案 目录 第1篇 ...
  • 对Java语言的每个语法都提供了一个或个例程讲解 大量使用流程图表示程序的执行过程,使用结构图表示程序的内部状态 每章最后都给出了典型的练习题,让读者及时练习,巩固提高,并提供了参考答案 目录 第1篇 ...
  • 代码出真知,这看不要紧,不仅让我对synchronized有更深入的理解,我还发现,网上许多文章,甚至很面试官的理解都是错误的。 比如,说说你对自旋的理解。你可以先把答案自己在心里想一下,看到下面,你可能...

    synchronized介绍

    synchronized相信大家都比较熟悉,面试的时候也是必备问题。有很多书籍介绍它,网上也有很多很多相关的文字,我自己也看过很多。但是看了就忘,印象不深,于是我决定阅读源码来加深记忆。代码出真知,这一看不要紧,不仅让我对synchronized有了更深入的理解,我还发现,网上许多文章,甚至很多面试官的理解都是错误的。

    比如,说说你对自旋锁的理解。你可以先把答案自己在心里想一下,看到下面,你可能会发现,原来你原来的理解是错误的。

    synchronized使用

    使用上我就不多说了,相信大家都知道,这里概括一下

    synchronized(obj) {
       ....
       some code
       ...
    }
    

    这种使用时对obj对象上锁。也是比较常见的使用。

    synchronized void test() {
        ....
        some code
        ...
    }
    

    这种事对this进行上锁

    synchronized static void test() {
        ....
        some code
        ...
    }
    

    这种事对Class对象上锁。

    synchronized字节码表示

    加入我们有一个简单的方法如下

    public void test() {
        Object obj = new Object();
        synchronized (obj) {
            System.out.println("in locking");
        }
    }
    

    会编译成以下字节码

     public test()V
       ...
        MONITORENTER
       L0
        LINENUMBER 15 L0
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        LDC "in locking"
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L6
        LINENUMBER 16 L6
        ALOAD 2
        MONITOREXIT
       L1
        GOTO L7
       L2
       FRAME FULL [com/pinduoduo/service/jmm/JmmTest java/lang/Object java/lang/Object] [java/lang/Throwable]
        ASTORE 3
        ALOAD 2
        MONITOREXIT
        ...
    }
    

    其他我们不要管,可以看到字节码中有一个MONITOREXIT指令和两个MONITOREXIT指令。两个MONITOREXIT是因为要异常的情况也也要进行MONITOREXIT.

    synchronized的jvm表示

    我们在上小节知道了synchronized块是有MONITORENTERMONITOREXIT指令包裹着的,那这两个指令怎么执行呢?这就涉及的jvm怎么实现的了,这里我们就以hotspot为例,看看他是怎么实现的。

    hotspot的这两个指令执行入口在bytecodeInterpreter.cpp中的CASE(_monitorenter)CASE(_monitorexit)中,接着就是一堆c++代码了,这里我们不深入,不然大家肯定睡着了,有兴趣的小伙伴可以自己去看看,我其他文章也会具体分析。

    synchronized原理

    这一小节会简单的说一说synchronized的原理,都是根据我阅读源码的总结。

    偏向锁

    偏向锁适用于没有多个线程会进入的资源。这个点在说轻量级锁的时候会再次提到。

    偏向锁又称无锁,因为它只改变锁对象头的markWord。

    在Hotspot中,每个java对象并不是对应一个c++对象,而是用oop-klass这种二分模型表示,关于这个二分模型我们也不展开说。oop中一个一个markOop,他的类型是 markOopDesc*,是一个指针。而markword就是指针的值,而不是指针指向的地址的值。啥意思呢?比如64位机器上,markOop就是一个64位的数值而已,这一点也是我饶了很久才懂。这里只是提一下,不重要。

    结构图如下:
    在这里插入图片描述

    偏向锁状态下,markword的锁标志位会变成01,是否偏向标志为1,此外epoch只一个标志,用于是否需要批量重定向的判断,我们先不管它。那如何证明偏向锁状态下,锁的markword会变成这个样子呢?我们的jol就登场了,跟我一起看接下来的小demo,耐心。

    首先我们引入jol的依赖

     <dependency>
         <groupId>org.openjdk.jol</groupId>
         <artifactId>jol-core</artifactId>
         <version>0.9</version>
    </dependency>
    

    代码如下

    import org.openjdk.jol.info.ClassLayout;
    import org.openjdk.jol.vm.VM;
    
    public class MarkWordTest {
    
        public static void main(String[] args) throws InterruptedException {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            MarkWordTest object = new MarkWordTest();
            String classLayout1 = ClassLayout.parseInstance(object).toPrintable();
            System.out.println("---------------------------加锁前---------------------------");
            System.out.println(classLayout1);
            System.out.println("------------------------------------------------------------");
            synchronized (object) {
                //打印当前jvm信息
                System.out.println("---------------------------加锁中---------------------------");
                String classLayout2 = ClassLayout.parseInstance(object).toPrintable();
                System.out.println(classLayout2);
                System.out.println("------------------------------------------------------------");
            }
            System.out.println("---------------------------加锁后---------------------------");
            String classLayout3 = ClassLayout.parseInstance(object).toPrintable();
            System.out.println(classLayout3);
            System.out.println("------------------------------------------------------------");
        }
    
    }
    

    首先注意到,代码开始有一个sleep 5秒的操作,是因为偏向锁默认有个延迟启动时间,默认为4s。

    然后分别在加锁前、中、后对对象头进行打印,我们看一下打印结果

    
    ---------------------------加锁前---------------------------
    com.pinduoduo.service.jmm.MarkWordTest object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    ------------------------------------------------------------
    ---------------------------加锁中---------------------------
    com.pinduoduo.service.jmm.MarkWordTest object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    ------------------------------------------------------------
    ---------------------------加锁后---------------------------
    com.pinduoduo.service.jmm.MarkWordTest object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    ------------------------------------------------------------
    
    Process finished with exit code 0
    
    

    markword是64位,既上面一堆数字的前两行,我们删掉其他东西,只看markword

    ---------------------------加锁前---------------------------
    ...
          0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    ...
    ------------------------------------------------------------
    ---------------------------加锁中---------------------------                     
    ...
          0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    ...
    ------------------------------------------------------------
    ---------------------------加锁后---------------------------
    ...
          0     4        (object header)                           05 38 67 03 (00000101 00111000 01100111 00000011) (57096197)
          4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
    ...
    ------------------------------------------------------------
    

    是不是有点看不懂,这是因为,Hopspot是小端储存,既低位值放在高位地址上。那我们按照我们好读的宗旨把它改为大端存

    ---------------------------加锁前---------------------------
    00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101
    ------------------------------------------------------------
    ---------------------------加锁中---------------------------                     
    00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101
    
    ------------------------------------------------------------
    ---------------------------加锁后---------------------------
    00000000 00000000 00000000 00000000 00000011 01100111 00111000 00000101
    ------------------------------------------------------------
    

    现在就是我们可以读懂的结构了。

    等等,加锁前为啥锁标志位为01,偏向锁模式呢?是的,是这样的,对象头初始就是偏向锁模式,并不是很多文章都说的无锁模式,只是现在还没有指向任何线程。

    加锁中我们就好理解了,锁标志位是偏向锁,并且线程id也不是0了。

    加锁后可以看到,markword并没有任何改变。这样做自然有用,就是,当有其他线程来想进入临界区的时候,看到偏向线程id不是自己,直接进行锁升级(不考虑批量重偏向)。

    至此我们可以得出一个结论:偏向锁只在只有一个线程会进入临界区的场景有用。

    不知道你有没有注意到一个问题,就是,既然退出临界区后,markword没有任何变化,那锁重入的计数怎么实现的?这就涉及到synchronized的重入了,不管是偏向锁、轻量锁还是重量级锁,都是使用的同一套重入机制。

    自旋锁的重入

    在Hopspot中,每个线程都有一个锁记录表,锁记录是BasicObjectLock类型,里边有一个obj和displaced_header,obj存储锁对象,set_displaced_header锁对象的markword,每一次进入临界区,都会创建一个BasicObjectLock对象,如果是重复,那displaced_header就置为null.

    在这里插入图片描述

    轻量级锁

    轻量级锁适用于线程交替进入临界区的场景,只要有锁竞争就会升级成重量级锁。注意,1.8中没有自旋锁,别再说经过自旋后升级成重量级锁!!!

    别嫌我啰嗦,再比较一下偏向锁与轻量级锁适用场景的不同。

    偏向锁:只有一个线程会进入临界区

    轻量级锁:不同线程交替进入临界区

    偏向锁和轻量级锁都是在没有锁竞争的条件下适用的。

    轻量级锁的加锁和释放都是通过cas操作对锁对象的markword就行修改而实现的,它的重入与偏向锁的机制相同,这里就不赘述了。

    重量级锁

    重量级锁的实现是用了监视器模式(moniter),我猜字节码指令MONITORENTERMONITOREXIT开始指的就是这个操作,后来经过一系列的优化,使这两个指令的字面意义跟锁的实现不太对应了。

    在Hotspot中,每个对象(java对象)都会关联一个ObjectMonitor,这个就是一个监视器。

    ObjectMonitor中我们重点关注以下几个字段

     ObjectMonitor() {
        _owner        = NULL; // 锁持有者
        _recursions   = 0; // 重入次数
        _WaitSet      = NULL;
        _cxq          = NULL ;
        _EntryList    = NULL ;
      }
    

    _owner既为锁持有者。

    _recursions表示重入次数。从这个字段就可以看出来,重量级锁的重入跟轻量级锁、偏向锁的重入不同,是用计数法来做的。

    _cxq是一个队列,如果线程获取锁失败,一定是进入这个队列的。

    _EntryList也是一个人队列,当ObjectMonitor持有者释放锁后,ObjectMonitor一定是从这个队列中唤醒一个线程的。

    _WaitSet是调用wait操作的线程队列。

    认识这几个字段后,我们会很容易理解ObjectMonitor的运作原来,并没有网上说的那么复杂,至于网上经常晒的那张流程图我就不贴了。下面我概括一下。

    • 线程要进入临界区的时候,如果_owner为空,则直接用cas去修改_owner的值,成功则证明获取锁成功,否则,会准备进去队列,在进入队列前,会经过多次自旋尝试cas修改_owner,这也是为什么synchronized,是非公平锁的原因,就是他会不断的抢占式的获取锁。

    • 多次尝试无果后,线程会进入_cxq队列中。注意,抢占无果的线程一定是进入_cxq队列。

    • 线程在获取锁后,如果调用lock#wait方法,那这个线程会进入_WaitSet队列。

    • 当获取锁的线程调notify或notifyAll时,根据不同的策略,_WaitSet会将节点插入_cxq_EntryList中。

      0: 将头节点插入到EntryList头部
      1: 将头节点插入到EntryList尾部
      2: 将头节点插入到cxq头部,默认选项
      3: 将头节点插入到cxq尾部
      

    当锁空闲的时候,根据不同的策略,以不同的方式将_cxq插入到_EntryList

    QMode = 2且cxq非空:取cxq队列队首的ObjectWaiter对象,调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回,后面的代码不会执行了;
    QMode = 3且cxq非空:把cxq队列插入到EntryList的尾部;
    QMode = 4且cxq非空:把cxq队列插入到EntryList的头部;
    QMode = 0:暂时什么都不做
    
    • 从队列中取线程来进入临界区:
      如果EntryList的首元素非空,就取出来调用ExitEpilog方法,该方法会唤醒ObjectWaiter对象的线程,然后立即返回;
      如果EntryList的首元素为空,就将cxq的所有元素放入到EntryList中,然后再从EntryList中取出来队首元素执行ExitEpilog方法,然后立即返回;
    • 如果是重入则会_recursions++

    总结

    • 偏向锁适用于只有一个线程会进入临界资源的情景,只要有多线程进入临界区就会导致锁升级
    • 轻量级锁适合多线程交替进入临界区的情景,只要有多线程同时想进入临界区就会导致锁升级
    • 偏向锁和轻量级锁的重入都是由线程中的锁记录来实现的
    • 重量级锁采用Moniter实现。根据不同策略和操作,线程在 _WaitSet_cxq_EntryList 中流转,重入通过_recursions计数来实现。
    展开全文
  •  对于第二个问题,像是比第一个具体一些,因为帖子作者已经提供了一个性能数据报告,但我仍然觉得通过这些数据没有办法准确地判断数据库是否有性能问题。比如你是一个医生,我让一个人站在你的面前测心率,结果是50...
  •  本书是本关于oracle database 9i、10g 和11g 数据库体系结构的权威图书,涵盖所有重要的oracle 体系结构特性,包括文件、内存结构和进程,和闩,事务、并发和版本,表和索引,数据类型,分区和并行,以及...
  • 第2章提供了一个创建Oracle数据库的绝好例子,从中你将深入地了解数据库和实例的概念。第3章介绍了各种类型的文件,特别是重做日志文件、闪回日志文件等。第4章关于内存,具体介绍了一些新选项,如何使用这些选项,...
  • 这是一本专门讲解Java并发的书,涉及到各种、常见安全的集合类,基本就是将 JUC(java.util.concurrent包的简称)里所有的内容覆盖了一遍,看完你一定有收获。强烈推荐! 上面推荐的几本书可能不太容易读懂,建议...
  • 基于Spring Boot实现图片上传/加水印一把梭操作 EVCache缓存在 Spring Boot中的实战 Guava Cache本地缓存在 Spring Boot应用中的实践 Spring Boot项目利用MyBatis Generator进行数据层代码自动生成 初探Kotlin+...
  • Bill Fenner AT&T实验室的主要技术人员,专攻IP播、网络管理和测量,他是IETF路由的领域主管之,负责审批作为RFC出版的所有路由相关文档。 Andrew M. Rudoff Sun公司的资深软件工程师,专攻网络、操作系统内核...
  • 本项目使用了大量Gif图片, 而且github在国内的访问速度非常不稳定,导致文章页面打开稍慢, 为了解决大陆用户访问项目速度慢的问题, zhaoolee在阿里云买了一台5M的VPS, 已将所有文章链接替换到v2fy.com域名下, 访问...
  • 阅读本书的过程,就是一些以前模糊地知道,但又觉得很难运用的理论实实在在地运用到实践中的过程。  虽然本书是针对SQL Server这个特定数据库平台来阐述理论和实践的,然而,本书所体现的思想和方法,完全可以...

空空如也

空空如也

1 2 3 4
收藏数 66
精华内容 26
关键字:

多了一把锁阅读答案