Synchronized关键字 详解

目录

前言

一、Synchronized作用

二、实现原理

1.字节码

2.MarkWord

3.ObjectMonitor

三: 锁的升级过程

1. 偏向锁的获取流程

2. 轻量级锁的获取流程

3. 重量级锁的获取和释放流程

四、锁是否可以降级以及什么时候会降级?

       1. Synchronized 除了锁升级之外是可以进行锁降级的。

五、参考代码

六、整体流程图

总结



前言

Synchronized

 1.  是否只能锁升级,不能锁降级。

 2.  实现的原理是什么?

 3.  Synchronized锁和JUC下的LOCK有什么区别?

     JUC下的AQS其实和Synchronized的重量级锁的实现,几乎是一模一样的,实现思想是一模一样的:  AQS中 Node state 以及锁的释放和获取中在线程挂起之前都是尽最大的努力不让线程挂起(多次的CAS操作),只是JUC下的锁更加灵活, 公平/非公平 读写锁 互斥锁 在此基础上又衍生了很多的同步类。

      个人认为: 实现细节有差异,但是实现思想几乎一模一样。


提示:以下是本篇文章正文内容,下面案例可供参考

一、Synchronized作用

Synchronized是Java提供的解决多线程并发访问(主要是修改操作)共享资源导致的数据不一致的问题。

二、实现原理

1.字节码

Synchronized修饰的方法或者代码块 在进行编译的时候会添加monitorenter  monitorexit字节码
其中monitorenter和monitorexit是成对出现而且 monitorexit会比monitorenter多一个。
 为了防止代码块的异常退出。
例如:
 1. 修饰代码块的时候 
         9: monitorenter         // 申请对象锁
        10: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: sipush        222
        16: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
        19: aload_1
        20: monitorexit      // 释放对象锁
        21: goto          29
        24: astore_2
        25: aload_1
        26: monitorexit   // 释放对象锁

  2. 修饰方法的时候:
          
    public static synchronized void print11(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED    

注意:

  1. 当线程执行到当前方法的时候将检测到ACC_SYNCHRONIZED的标志为1,会自动的在方法的前后添加 monitorenter和monitorexit指令,可以称之为monitor指令的隐式调用。

  2. 在JVM进行了锁的优化之后,出现了锁的升级过程即:

无锁/匿名偏向锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 以及在锁升级的过程中采用CAS+自适应的自旋次数进行优化。以及后面会讲到的锁的降级。所以在执行指令monitorenter的时候,并不一定是申请获取对象锁。以及并不是在加锁的时候就一定会产生线程上下文的切换(用户态和内核态的转变)。

3. 总之: 无论是修饰方法还是修饰代码块,最都是通过monitor指令实现。只是在执行monitor指令的时候在不同的阶段会有不同的操作。

2.MarkWord

Java中对象的组成:

    对象头  实例数据  对其填充  三大部分组成 。

    对象头是由 markWord 类型指针  数组长度(如果是数组对象)。其中MarkWord(32bit的操作系统中 )包含了

    对象的GC年龄(4bit) 偏向锁标志(1bit) 锁的类型(2bit)其他信息(hashCode/ 锁的信息 23bit) Epoch(2bit)

64bit操作系统下不同锁状态下MarkWord中存储的信息

3.ObjectMonitor

在Synchronized升级为重量级锁时,JVM会为每一个锁对象创建唯一的一个监视器对象,完成Synchronized的锁实现。


// 以下列出的是监视器对象中包含的几个个人认为比较重的变量,不包含全部的变量

ObjectMonitor {

   EntryList;  // 获取锁的缓冲区,用来将waitSet和Cxq队列中节点移动到enteyList中进行排队。统一等待释放锁的线程唤醒

   CXQ;       // 线程第一次没有获取到锁,将会进入队列中进行排队等带获取锁

   waitSet; // 线程已经获取过一次锁,但是由于调用Object中的 wait()/wait(time)方法,等待别的线程调用notify/notifyAll方法唤醒或者wait的时间到了之后自动唤醒

   owner;  // 当前持有锁的线程地址,用于判断当前是那个线程持有锁

   recursions; // 锁的重入次数

   header; // 指向对象头中的MarkWord,方便在ObjectMonitor销毁的时候将之前的Markword信息还原到对象中

   object; //  指向锁对象

   
}

三: 锁的升级过程

1. 偏向锁的获取流程

   1. 没有开启偏向锁(JVM默认,也是JVM的一种优化,后面会讲) 001,会直接获取轻量级锁,没有偏向锁的流程。锁升级: 无锁 ------> 轻量级锁

   2. 如果开启偏向锁 101

           1). 获取锁标志为是否为 101 ,如果不是会升级为轻量级锁

           2). 判断对象头中的偏向线程ID是否为空 。

                 如果为空. CAS修改对象头中的偏向线程ID为当前线程ID

                            修改成功: 拿到锁

                            修改失败: 说明此时已经出现了锁的竞争,但是已经有线程拿到了偏向。锁,此时会进行偏向锁的撤销。()

                 如果不为空  会判断偏向线程ID是否为当前线程ID

                        如果是       则进行锁的重入 

                        如果不是    同样进行偏向锁的撤销

           3).    在进行以上两种情况下的偏向锁撤销会非常的消耗性能。 在撤销偏向锁的时候会

    将获得偏向锁的线程暂停(STW)。生成轻量级锁的Lock Reorder记录。如果频繁的出现偏向锁的撤销,会消耗大量的系统性能。所以JVM会默认禁用偏向锁。

           4).   撤销偏向的时候会等到全局安全点(没有字节码在执行),暂停拥有偏向锁的线程,如果偏向锁线程退出了同步代码块,则会将对象头修改为无锁不可偏向的状态。

           如果没有退出同步代码块,则会升级为轻量级锁,之前持有偏向锁的线程会升级为轻量级锁

           5).   偏向锁住要使用来解决当前的同步代码块只有在单线程下运行,如果业务代码符合要求可以开启偏向锁提升性能。(因为其他锁的获取流程会有大量的CAS)

   3.   如果在获取锁之前已经执行了Object的原生的hashCode方法(对象没有重写hashCode方法),也不会进行偏向锁的获取,锁会直接进入轻量级锁的状态,因为markWord存储HashCode。没有空间存储偏向线程ID。如果在获取锁之后执行hashCode方法,偏向锁会直接膨胀为重量级锁

偏向锁在释放之后,只会退回到初始状态即 无锁/匿名偏向锁状态(无论是否执行hashCode方法)

2. 轻量级锁的获取流程

  • 在锁升级为轻量级锁的时候,线程会在线程栈中创建一个Lock Record,然后通过CAS操作将对象头中的MarkWord指向当前线程栈中创建的Lock Record。
  • 如果CAS操作失败,会进行自适应自旋一定次数之后,如果还没有获取到锁就会将锁升级到重量级锁,然后将升级到重量级锁的线程挂起,等到之前获取轻量级锁的线程释放锁失败之后,然后执行重量级锁过程
  • 轻量级锁的释放: 将Lock Reorder中的保存的MarkWord信息CAS回写到对象头中 
  •  在线程获取轻量级锁的时候,对象头中的hashCode GC年龄等信息保存在线程栈的锁记录中,锁释放之后写回

3. 重量级锁的获取和释放流程

  • 重量级锁的实现是通过ObjectMonitor对象监视器实现,会进行线程的阻塞(用户态和内核态的切换),为了避免线程直接进行阻塞,在获取重量级锁的时候会进行多次的自旋操作,如果最后都没有获取到锁,才会进行线程挂起
  • 判断是否已经有线程拿到了锁 即owner是否为空

          如果为空, CAS获取锁,如果获取成功 执行代码,获取失败会进行自旋等待

          如果不为空 ,检查是否是锁重入,如果不是锁重入,自旋等待

  •    如果之前的自旋失败,会再进行一次TryLock获取锁,如果失败,还会再进行一次自旋操作,如果自旋失败,则重量级锁的获取中的所有的自旋操作到此结束
  •  经过多次自旋操作,依然没有获取到锁,会将线程的进入队列等待被唤醒

            1. 将线程封装成ObjectWaiter节点,通过CAS操作加入到ObjectMonitor的cxq队列的首部,修改_cxq指向当前节点

            2. 加入节点成功之后,还会再进行一次TryLock尝试获取锁,如果获取锁失败,会将线程挂起。

  •      到此线程获取重量级锁的流程结束,要么经过多次的自旋和CAS操作成功获取到锁(非公平), 要么获取锁失败,加入到CXQ的队列中(首部插入),线程挂起。
  • 重量级锁的释放:在释放的时候根据不同的Qmode值采用不同的唤醒策略,主要是对ObjectMonitor中的EntryList和CXQ队列的不同操作
  •  在重量级锁释放的时候,如果刚好有线程获取锁,那么就不会进行唤醒之前在排队的节点,由当前线程获取到锁
  • 如果没有线程获取锁成功,需要唤醒队列中的节点
  • Qmode == 2 && CXQ!=null
  1.  优先从CXQ队列中唤醒线程,从首部开始唤醒
  • Qmode == 3 && CXQ!=null
  1.  将CXQ队列尾插法插入到EntryList
  • Qmode == 4 && CXQ!=null
  1.  将CXQ队列头插法插入到EntryList
  •     完成以上三步的操作之后
  •  如果CXQ==null,EntryList!=null,从EntryList队列中唤醒
  •  如果CXQ==null EntryList==null,continue执行直到CXQ不为空或者EntryList不为null
  •  只有EntryList等于null的时候,而且Cxq不为空的时候才会执行下面的操作  
  1. Qmode == 1  

           将倒置CXQ队列,加入到EntryList中,然后从EntryList中唤醒线程

  1. Qmode!=1

           将CXQ队列,加入到EntryList中,然后从EntryList中唤醒线程         

          

如果调用了Object中的wait方法,那么节点会转移到waitSet队列中,只有再次调用notify/notifyAll 方法 才能将waitSet队列中的头节点或者全部节点加入到EntryList中,然后在由线程释放锁之后进行唤醒。至于什么时候能够抢到锁,需要看waitSet中节点是如何移动的

  •     Policy=0:将ObjectWaiter放入到enteylist队列的排头位置
  •     Policy=1:放入到entrylist队列末尾位置
  •     Policy=2(默认):判断entrylist是否为空,为空就放入到entrylist中,否则放入到cxq队列的排头位置
  •     Policy=3:判断cxq是否为空,如果为空,直接放入头部,否则放入cxq队列末尾位置

总结: notify方法并不一定能够立刻唤醒调用Wait的线程。

线程的notify/nofityAll方法在jvm源码中并没有唤醒线程,而是从waitSet链表取出一个节点进行挪动,等到真正出了synchronized代码块时,根据QMode策略进行唤醒操作。


四、锁是否可以降级以及什么时候会降级?

       1. Synchronized 除了锁升级之外是可以进行锁降级的。

  •          在获取锁之后所有需要获取锁的线程已经执行完毕之后,空闲一段时间之后,对象会变成 无锁或者匿名偏向锁的状态。并不是只要锁升级了之后就不能进行锁的降级,而之后的每一次都会获取升级后的锁。
  •          在获取锁的过程中只能进行锁的升级而不能降级

五、参考代码

 整体代码如下 可以自行修改hashCode方法的执行时机以及配置启动过参数

-XX:+UseBiasedLocking   开启偏向锁 默认JVM是禁用

-XX:BiasedLockingStartupDelay=0   偏向锁的延时启动时间 0 立即启动 

final static Object lock = new Object();
    
    public    void  print(String des){

        System.out.println(des + " ===");
        System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
    
    @Test
    public  void  test(){
        print("初始化");
        int hashCode = lock.hashCode();
        print("调用HashCode方法  hashcode = " + hashCode);

        synchronized (lock){
              print("获取到锁之后");
        }
        print("锁释放之后");
        new Thread(()->{
            synchronized (lock){
                print("111");
                try {
                    lock.wait(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            print("111释放之后");

        }).start();
        new Thread(()->{

            synchronized (lock){
                print("222");
                try {
                    lock.wait(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            print("222释放之后");

        }).start();

        try {
            TimeUnit.MINUTES.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        print("没有线程竞争之后");
    }

六、整体流程图

 

总结

  纸上得来终觉浅,绝知此事要躬行。

  

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值