深入理解wait()、notify()和notifyAll()方法为什么属于Object,为什么要在synchronized代码块中

        LZ在网上看了很多关于这个问题的解释,都不够深入,那么今天就让我带大家深入了解这个问题。关于synchronized的详细介绍请移步大神所写的博客:深入理解Java并发之synchronized实现原理,这篇文档稍微有点长,我会用自己的话总结一下关于wait()、notify()和notifyAll()的问题。

        说到Object的这三个方法,就需要先说一下synchronized关键字,我们知道每一个实例对象或是类对象都可以作为一把锁来使用,例如:

public class SynchronizedTest{
    private static int i = 0;

    public static synchronized void increase(){  //加锁的对象是SynchronizedTest的类对象
        i++;
    }

    public synchronized void increase4Obj(){  //加锁的对象当时调用该方法的实例对象
        i++;
    }

    public void increase2Obj(){
        synchronized (Object.class){   //加锁的对象是Object的类对象
            i++;
        }
    }
}

       上面方式是synchronized实现锁的三种方式,这一点不懂的还请看一下我上面提到的大神写的博客,之所以每个对象都可以当成一把锁把使用,是因为每一个对象都有唯一的一个monitor对象与之关联,JVM中对象的布局如下(直接复制大神博客里面的图片)

                         

 

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

       其中在Mark Word中的信息会根据锁的状态进行动态的变换,当锁的状态为重量级锁的时候,Mark Word中会有一个引用指向monitor对象(每一个对象都会对应一个monitor对象),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 ;
  }

       其中比较关键的变量有

           _count:记录当前持有monitor对象的线程重入的次数

           _owner:记录当前持有monitor对象的线程

           _WaitSet:等待集合,当前持有monitor对象的线程,调用wait()方法会释放monitor对象锁,然后进入该集合等待被唤醒

           _EntryList:等待获取锁的集合,在进入synchronized方法或synchronized修饰的代码块时,竞争获取monitor对象锁失败的线程会进入该集合等待机会竞争monitor对象锁。

       其中的关系如下图所示:

                

    知道了synchronized的原理后,就可以回答上面两个问题了:

           一:wait()、notify()和notifyAll()方法为什么要在synchronized代码块中?

           在Object的wait()方法上面有这样一行注释:The current thread must own this object's monitor,意思是调用实例对象的wait()方法时,该线程必须拥有当前对象的monitor对象锁,而要拥有monitor对象锁就需要在synchronized修饰的方法或代码块中竞争并生成占用monitor对象锁。而不使用synchronized修饰的方法或代码块就不会占有monitor对象锁,所以在synchronized代码块之外调用会出现错误,错误提示为:

Exception in thread "main" java.lang.IllegalMonitorStateException
    at java.lang.Object.wait(Native Method)
    at java.lang.Object.wait(Object.java:502)
    at it.cast.basic.thread.SynchronizedTest.main(SynchronizedTest.java:27)

           因为只有拥有了monitor对象的线程才能调用wait()方法进入_WaitSet 等待集合中等待被唤醒,而且对于monitor对象来说,同一时刻只能被一个线程锁持有,在不加synchronized修饰的时候(也就是不是有锁的时候),monitor对象就不会被使用到,这里突然使用wait()方法就是出现逻辑错误,所以必须在synchronized代码块中。

        二:wait()、notify()和notifyAll()方法为什么属于Object?

           因为每一个对象都可以被当作一把锁来使用,对象在JVM中的内存划分为对象头和实例变量,其中对象头里面有包含了对象的hashcode、锁信息和分代年龄等信息,对象头会根据锁状态的不同,动态的变换,当锁升级为重量级锁的时候,对象头会拥有一个monitor对象的引用,monitor对象也就是每一个对象实现锁的关键,当所有的线程都竞争monitor对象锁,只有一个线程能成功占有锁,其他线程都会进入阻塞进入等待集合中,成功占有锁的线程会接着执行代码,在执行代码的过程中,如果遇到wait()方法,当前线程会释放掉monitor对象锁,然后进入monitor对象的等待被唤醒的集合,唤醒的动作是通过notify()和notifyAll()来实现的。

发布了221 篇原创文章 · 获赞 30 · 访问量 4万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章