美文网首页
java synchronized锁原理

java synchronized锁原理

作者: 会跳的八爪鱼 | 来源:发表于2023-05-30 21:13 被阅读0次
    synchronized用法

    synchronized关键字用于给代码加锁,防止多线程并发问题。可以用在方法或者代码块上
    synchronized关键字用在方法上

    public synchronized void test1() {
       System.out.println("---test1---");
    }
    public synchronized static void test2() {
       System.out.println("---test2---");
    }
    public void test3() {
       System.out.println("---test3---");
    }
    public static void test4() {
       System.out.println("---test4---");
    }
    

    synchronized用在普通方法上(如test1())表示对对象加锁,同一个对象同时调用这个方法会被阻塞。
    synchronized用在静态方法上(如test2())表示对类对象加锁,同一个类的不同对象同时调用这个方法就会被阻塞。
    但是同一个对象同时调用test1()和test2()不会阻塞,因为这两个方法是对不同对象加锁的
    如果同时调用test1()和test3()也不会阻塞,因为test3()没有被synchronized修饰,不存在加锁逻辑。

    synchronized关键字用在代码块上

    public synchronized void test1() {
       System.out.println("---test1---");
    }
    public synchronized static void test2() {
       System.out.println("---test2---");
    }
    public void test3() {
       System.out.println("---test3---");
    }
    public static void test4() {
       System.out.println("---test4---");
    }
    

    字节码分析
    ①synchronized作用在代码块上

    public static void main(String[] args) {
           String s = "aa";
           synchronized (s){
               System.out.println("------");
           }
    }
    
    synchronized作用在代码块上

    如图,synchronized正是通过monitorenter进入同步块,锁计数器加1,monitorexit退出同步块,锁计数器减1。图中出现两个monitorexit是因为如果在同步代码块中出现异常,也要能够释放锁。
    ②synchronized作用在方法上

    public synchronized void test() {
       System.out.println("------");
    }
    
    synchronized作用在方法上

    如图,此时synchronized虽然没有使用monitorenter和monitorexit命令,但是会在方法上添加ACC_SYNCHRONIZED标志,这个标志与monitorenter和monitorexit命令作用相同。

    对象头信息分析
    java对象布局
    如上图,java对象存储了三部分内容,对象头,实例数据,填充对齐

    ①对象头包含mark word和klass pointer

    • mark word存储了对象的哈希码,gc年龄,是否加锁,是否偏向锁,直接锁指针等
    • klass pointer存储了对象的类信息
    • 如果对象是数组类型的话,还需要存储数组的长度

    mark word信息在64位JVM中的布局如图所示:

    对象头信息
    上面我们说过synchronized关键字会给对象加锁,其实就是修改对象头中的信息。从上面的图可以看出,java对象主要分为以下几种状态:无锁,偏向锁,轻量级锁,重量级锁,这几种锁的状态性能各不相同
    gc的年龄占4位,这也可以解释对象在gc中标志复制的次数不能超过15次。

    ②实例数据存储了对象的属性信息,父类信息等
    ③填充对齐,对象必须是8字节的倍数,因为如果不是8字节的倍数,cpu在进行读写的时候会出现跨缓存行的数据,会降低程序的执行效率。

    我们可以使用JOL来分析java的对象布局,添加依赖

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

    测试代码:

    class TestA{
       private int a = 33;
    }
    public class SynchronizedTest {
       public static void main(String[] args) {
           TestA aa = new TestA();
           // ①打印原始的对象头信息
           System.out.println("----------------");
           System.out.println(ClassLayout.parseInstance(aa).toPrintable());
           System.out.println(aa.hashCode());
           // ②打印带有hashcode的对象头信息
           String[] ss = new String[2];
           ss[0] = "aa";
           ss[1] = "bb";
           System.out.println(ClassLayout.parseInstance(ss).toPrintable());
       }
    }
    

    其中OFF表示对象的相对地址,SZ表示所占字节的长度,TYPE DESCRIPTION 表示描述信息,VALUE表示该行的值。
    例如下图是上面①中打印的结果。
    0-8表示mark word信息,其中因为没有输出哈希码,而且没有加锁,所以值为0x0000000000000001 ,
    8-12的地址表示指向的类信息。由于含有属性信息。
    12-16表示属性信息,如果有多个属性,也会依次展示出来。
    由于aa对象所占的总字节数刚好等于16,是8的倍数,所以不用字节填充。

    #普通对象打印的对象信息,non-biasable表示这时无锁状态下的对象信息
    com.java.TestA object internals:
    OFF  SZ   TYPE DESCRIPTION               VALUE
     0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
     8   4        (object header: class)    0xf800c145
    12   4    int TestA.a                   33
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
    #数组对象打印的对象信息
    [Ljava.lang.String; object internals:
    OFF  SZ               TYPE DESCRIPTION               VALUE
     0   8                    (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
     8   4                    (object header: class)    0xf800372e
    #此处由于是数组对象,对象头中包含数组长度信息
    12   4                    (array length)            2
    12   4                    (alignment/padding gap)   
    16   8   java.lang.String String;.<elements>        N/A
    Instance size: 24 bytes
    Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
    
    monitor监视器

    每个java对象都有一个monitor监视器,当一个monitor被持有后,该java对象将处于锁定状态


    monitor监视器

    执行流程分析:
    ①cxq、EntryList都是先进后出队列FILO
    ②争抢锁失败的线程会进入cxq
    ③获取锁的线程调用wait后进入waitSet
    ④waitSet中被notify唤醒的线程会进入cxq
    ⑤持有锁的线程释放锁后,唤醒EntryList中的线程
    ⑥如果EntryList没有节点,则会将cxq的节点移动过来,再唤醒队中的线程

    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 ;
    }
    
    加锁过程分析

    偏向锁

    当只有一个线程执行synchronized代码块时,此时给对象添加的是偏向锁,获取偏向锁的线程只是将对象的mark word设置为偏向状态,并设置获取到锁的线程ID和Epoch字段。
    当再次需要获取锁时,只需要判断是否是偏向锁以及线程ID是否是自己的即可。

    通过ClassLayout打印对象信息可得:

    public class SynchronizedTest {
       public static void main(String[] args) throws Exception {
           //jdk8需要延迟4s,才能开启偏向锁
           TimeUnit.SECONDS.sleep(5);
           String s = "aa";
           synchronized (s) {
               System.out.println(ClassLayout.parseInstance(s).toPrintable());
        }
    }
    # biased表示添加偏向锁
    OFF  SZ     TYPE DESCRIPTION               VALUE
     0   8          (object header: mark)     0x0000000003489005 (biased: 0x000000000000d224; epoch: 0; age: 0)
     8   4          (object header: class)    0xf80002da
    12   4   char[] String.value              [a, a]
    16   4      int String.hash               0
    20   4          (object alignment gap)    
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

    notes:为什么会有这个延迟,那是因为jvm在启动后,后台线程会竞争资源。如果直接开启偏向锁,会导致这些后台线程在竞争锁的过程中失败发生锁升级,而锁升级会增加资源消耗。

    轻量级锁
    如果有两个线程同时竞争锁资源,那么synchronized就会添加轻量级锁。

    1)首先将锁对象的mark word信息拷贝到自己的线程帧(LockRecord)中。拷贝成功后使用CAS更新mark word 锁记录为当前线程的指针;
    2)如果更新成功就表示当前线程持有了改对象的锁,如果更新失败,就表示当前锁存在竞争,此时不是立即升级到重量级锁,而是继续尝试(自旋)获取轻量级锁;
    3)当自旋获取锁失败多次后,就升级为重量级锁。

    为什么升级为轻量锁时要把对象头里的Mark Word复制到线程栈的锁记录中呢?
    因为在申请对象锁时 需要以该值作为CAS的比较条件,同时在升级到重量级锁的时候,能通过这个比较判定是否在持有锁的过程中此锁被其他线程申请过,如果被其他线程申请了,则在释放锁的时候要唤醒被挂起的线程。

    适用范围:锁竞争不激烈或者同步代码块执行时间较短的情况。

    public class SynchronizedTest {
       public static void main(String[] args) throws Exception {
           String s = "aa";
           synchronized (s) {
               System.out.println(ClassLayout.parseInstance(s).toPrintable());
        }
    }
    # thin lock表示添加轻量级锁
    OFF  SZ     TYPE DESCRIPTION               VALUE
     0   8          (object header: mark)     0x0000000002def3c8 (thin lock: 0x0000000002def3c8)
     8   4          (object header: class)    0xf80002da
    12   4   char[] String.value              [a, a]
    16   4      int String.hash               0
    20   4          (object alignment gap)    
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

    重量级锁
    重量级锁适用于锁竞争激烈或同步块执行时间长的场景

    如果多个线程同时竞争锁或者轻量级锁在多次自旋获取锁失败后会升级为重量级锁,获取锁失败后当前线程会阻塞;释放锁以后会唤醒阻塞线程。那么为什么这个是重量级锁呢?主要是因为线程的阻塞和唤醒涉及到线程切换以及系统调用引起的用户态和内核态切换等。
    重量级锁争抢的是上面所说得monitor监视器对象。监视器本质又是依赖于底层的操作系统的Mutex Lock来实现的。

    public class SynchronizedTest {
       public static void main(String[] args) throws Exception{
           String s = "aa";
           Thread t1 = new Thread(()->{
               synchronized (s) {
                   try {
                       TimeUnit.SECONDS.sleep(5);
                       System.out.println(ClassLayout.parseInstance(s).toPrintable());
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
           });
           Thread t2 = new Thread(()->{
               synchronized (s) {
                   System.out.println(ClassLayout.parseInstance(s).toPrintable());
               }
           });
           Thread t3 = new Thread(()->{
               synchronized (s) {
                   System.out.println(ClassLayout.parseInstance(s).toPrintable());
               }
           });
           t1.start();
           t2.start();
           t3.start();
    
           TimeUnit.MINUTES.sleep(1);
       }
    }
    # fat lock表示添加重量级锁
    OFF  SZ     TYPE DESCRIPTION               VALUE
     0   8          (object header: mark)     0x000000001d205f8a (fat lock: 0x000000001d205f8a)
     8   4          (object header: class)    0xf80002da
    12   4   char[] String.value              [a, a]
    16   4      int String.hash               0
    20   4          (object alignment gap)    
    Instance size: 24 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    

    java对于synchronized关键字的优化包括以下几点:
    锁升级
    三种锁性能比较:偏向锁>轻量级锁>重量级锁。当线程没有竞争时使用偏向锁,当线程开始竞争时升级为轻量级锁,当线程竞争激烈时,升级为重量级锁。但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。
    锁粗化
    锁粗化是通过扩大锁的范围,避免反复加锁和释放锁,因为连续加锁解锁操作,会导致不必要的性能损耗。比如下面这段代码就不会在对s加锁

    public static void main(String[] args) {
       String s = "aa";
       synchronized (s){
           System.out.println("------");
           synchronized (s){
               System.out.println("**********");
           }
       }
    }
    

    锁消除
    锁消除是指编译器如果认为这段代码不可能存在共享数据竞争,将会在运行时取消加锁逻辑。

    notes:通过mark word布局可知,如果打印了对象的hashcode信息之后再进行加锁,此时mark word已经没有填写epoch信息的地址,所以此时会直接升级为轻量级锁。

    参考:Synchronized关键字和锁原理
    synchronized详解
    java对象内存结构分析
    synchronized底层原理

    相关文章

      网友评论

          本文标题:java synchronized锁原理

          本文链接:https://www.haomeiwen.com/subject/uohleltx.html