synchronized原理(一) -- Java对象头及Monitor



前言

提示:本博客仅为本人学习记录笔记使用,有错误可以进行指正。

Java中存在一个synchronized关键字, 它是用来保证线程安全的一个机制 , 但本人一直对其原理没有深入研究过, 故而写这个synchronized原理系列来简单介绍一下其原理,供以后回头复习并发知识用。


前置知识

在真正介绍synchronized的概念之前,首先需要了解几个知识,Java对象头和Monitor监视器锁的知识。本篇就先来简单介绍一下这两个概念。


一、Java对象头

什么是Java对象头呢,可能大家都知道在Java中生成出来的对象会存放在JVM的堆区中,它们是线程共享的。这些对象可能由很多结构组成。而对象头就是其中的一部分结构, 是对象的头部结构,所以叫Java对象头(Object Header)。下图简单展示了Java对象的一个结构. 可以看到对象结构由三部分组成。对象头,实例数据以及对齐填充。
在这里插入图片描述
那么对象头的详细结构又是怎么样的呢? 下面阐述的结构32位JVM为准。普通对象的对象头由两部分组成:

一、Mark Word (32bits)
二、Klass Word (32bits)

而数组对象的对象头由三部分组成:

一、Mark Word (32bits)
二、Klass Word (32bits)
三、Array Length (32bits)

下面来简单介绍一下这三部分的概念。

1.Mark Word

Mark Word 在对象头中是一个很重要的结构。它标识了一个对象在不同加锁状态下的一个结构。那么Mark Word在32位虚拟机下的结构有哪几种情况呢。下图展示了Mark Word对应的结构。
在这里插入图片描述
可以看到在不同的加锁状态下,对应的MarkWord的结构是不一致的,具体每个锁状态对应的结构会在后面的系列详细阐述,此处先有个印象即可。

2.Klass Word

Klass Word可以简单理解成为指向该对象对应的方法区类对象的一个指针。通过这个指针就能够标识该类是哪个类型的数据,这里就不再过多介绍了。

3.Array Length

Array Length这个结构翻译过来就是数组长度。所以此部分由数组对象特有,是用来标识数组的长度的结构。

二、Monitor(监视器锁)

1.Monitor简介

Monitor可以被翻译成监视器或管程,每一个Java对象都会关联一个Monitor对象 (重要)。我们平时在使用synchronized关键字对一个对象加锁(重量级)时,其流程是与这个Monitor对象息息相关的。所以了解Monitor对象,可以帮助我们很好的理解synchronized的原理。话不多说,直接开整。

首先来看下Monitor对象的结构,如下图所示:
在这里插入图片描述

可以看到绿色部分对应的Monitor对象的核心组成部分有如下几个:

一、Owner
二、EntryList
三、WaitSet

Owner: 它标识了当前的持有Monitor对象的线程信息。

EntryList: 可以把它理解成一个阻塞队列,因为同一时间只有一个线程能争抢到锁,那么没有争抢到锁的线程就会被阻塞(状态变成Blocked),也就是加入到Monitor对象的EntryList中,等待Owner中的线程释放锁后,再去重新争抢锁。

WaitSet: 翻译过来叫等待集合,当持有锁的线程调用了锁对象的wait方法时, 因为wait方法会去释放当前锁,所以当前线程就会从Owner退出来,加入到WaitSet中,并且状态变成Waiting状态,等待其他的线程重新唤醒。

2.样例分析

上面介绍了一下Monitor的大致结构,现在让我们再来看一段代码,然后借助这个例子来阐述一下大致的流程,代码如下:

public class Main {

    // 一个锁对象
    private Lock lock = new Lock();

    // 共享变量
    private int num = 0;

    public static void main(String[] args) {
        Main main = new Main();

        // 创建两个线程调用main对象的testLock方法
        Thread t1 = new Thread(main::testLock, "t1");
        Thread t2 = new Thread(main::testLock, "t2");

        // 启动t1,t2两个线程
        t1.start();
        t2.start();

        try {
            // 此处调用t1,t2的join方法,保证两个线程结束后,在执行下面的代码
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 输出num, 若线程安全, 则num为10000. 若线程不安全, 则num不为10000
        System.out.println("num最终为:" + main.num);
    }

    // 测试synchronized的一个方法
    public void testLock() {
        // 当前线程对lock对象加锁
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + "对lock上了锁");
            // 临界区代码, 对num加1, 5000次
            for(int i = 0; i < 5000;i++) {
                this.num++;
            }
        }
        System.out.println(Thread.currentThread().getName() + "对lock释放了锁");
    }
}

class Lock {

    public Lock() {

    }
}

结果如下: 可以看到t1, t2线程只能同时有1个线程先执行完临界区的代码

t1对lock上了锁
t1对lock释放了锁
t2对lock上了锁
t2对lock释放了锁
num最终为:10000

再来对比一下如果不加synchronized关键字的结果: t1, t2线程会同时执行临界区的代码。就会导致线程不安全问题

t2对lock上了锁
t1对lock上了锁
t2对lock释放了锁
t1对lock释放了锁
num最终为:9358

流程分析: 可以看到代码中有两个线程t1, t2。它们在testLock()方法中使用synchronized尝试去给lock对象加锁,并执行num自增5000次的操作。假设线程t1先执行到了synchronized(lock) 这行代码,那么t1首先会去检查lock对象对应的Monitor对象的Owner是否已经被其他线程所占用,如果没有被占用,则t1线程就变成Owner, 在t1线程没释放锁之前,任何线程都不能再成为该lock对象对应的Monitor的Owner。接着t2线程执行testLock()方法,然后再继续走上面提到的流程,发现Monitor对象的Owner已经存在别的线程了,那么t2线程只能先加入到EntryList中,并且状态为Blocked(阻塞),直到t1线程释放锁后,再去争抢锁。若争抢到了,则t2就成为了Owner,在去继续执行相应的临界区代码, 否则还是在EntryList中阻塞,下图简单展示了一个大概的流程,读者可以结合下图理解这段话的含义。当然我上面的例子只有两个线程, 读者可以去思考有N个线程的场景,举一反三。

在这里插入图片描述

这里在简单的提一下,如果某个线程成为了Monitor对象的Owner。那么这个线程持有的锁对象的Mark Word就会变成上述的重量级锁状态对应的结构。也就是下图的结构。 其中前30位字节代表的是指向monitor对象的一个指针, 后两位10代表的是重量级锁的状态。而原本的hashcode等信息会被存入到Monitor对象中。等到线程释放锁的时候再被重置回来。
在这里插入图片描述

好了,如果看到这里的话相信大家已经对java对象头和Monitor对象已经有一个大致的了解了。有些博文可能会以字节码的角度阐述加锁的流程。字节码指令中的monitorenter就是去关联monitor对象的流程。monitorexit就是线程释放锁并且重置MarkWord的流程. 如果synchornized中出现异常,也会出现monitorexit指令去释放锁,下图是testLock方法的字节码指令,可以看到6是monitorenter指令。而63,69是monitorexit指令,分别代表关联monitor对象,正常退出释放锁和出现异常释放锁的情况。唔,这里笔者技术不够,就不再继续班门弄斧了。

 public void testLock();
    Code:
       0: aload_0
       1: getfield      #4                  // Field lock:LLock;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
      10: new           #19                 // class java/lang/StringBuilder
      13: dup
      14: invokespecial #20                 // Method java/lang/StringBuilder."<init>":()V
      17: invokestatic  #26                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
      20: invokevirtual #27                 // Method java/lang/Thread.getName:()Ljava/lang/String;
      23: invokevirtual #22                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      26: ldc           #28                 // String 对lock上了锁
      28: invokevirtual #22                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      31: invokevirtual #24                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      34: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      37: iconst_0
      38: istore_2
      39: iload_2
      40: sipush        5000
      43: if_icmpge     62
      46: aload_0
      47: dup
      48: getfield      #5                  // Field num:I
      51: iconst_1
      52: iadd
      53: putfield      #5                  // Field num:I
      56: iinc          2, 1
      59: goto          39
      62: aload_1
      63: monitorexit
      64: goto          72
      67: astore_3
      68: aload_1
      69: monitorexit
      70: aload_3
      71: athrow
      72: getstatic     #18                 // Field java/lang/System.out:Ljava/io/PrintStream;
      75: new           #19                 // class java/lang/StringBuilder
      78: dup
      79: invokespecial #20                 // Method java/lang/StringBuilder."<init>":()V
      82: invokestatic  #26                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
      85: invokevirtual #27                 // Method java/lang/Thread.getName:()Ljava/lang/String;
      88: invokevirtual #22                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      91: ldc           #29                 // String 对lock释放了锁
      93: invokevirtual #22                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      96: invokevirtual #24                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      99: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     102: return


总结

提示:重申一遍,本博客仅为本人学习记录笔记使用,有错误可以进行指正。

本篇作为笔者的第一篇博客,也是力求以能让大家理解的话术去编写了这篇文章。相信大家仔细阅读后,一定能够充分了解一波Java对象头和Monitor对象的知识。笔者也只是一个在奋力挣扎的菜鸡,希望大家互相共勉吧。

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值