【JUC】十四、synchronized进阶:Monitor

本文详细解释了Java中`synchronized`的关键字,涉及对象锁与类锁的区别,以及Monitor(管程)的作用。通过实例分析了synchronized在不同场景下的行为,包括对象锁的优先级、静态同步与实例同步的区别,以及Monitor在底层的实现机制。
摘要由CSDN通过智能技术生成

1、synchronized

写个demo,具体演示下对象锁与类锁,以及synchronized同步下的几种情况练习分析。demo里有资源类手机Phone,其有三个方法,发短信和发邮件这两个方法有synchronized关键字,另一个普通方法getHello。

class Phone {
    public synchronized void sendSMS() throws Exception {
        //TimeUnit.SECONDS.sleep(4);
        System.out.println("------sendSMS");
    }

    public synchronized void sendEmail() throws Exception {
        System.out.println("------sendEmail");
    }

    public void getHello() {
        System.out.println("------getHello");
    }
}

然后启动两个线程AA和BB,且二者进入就绪状态中间休眠100ms,给AA一个先抢夺CPU时间片的优势。

public class Lock8 {

    public static void main(String[] args) throws InterruptedException {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"AA").start();

        /**
         * start后线程进入的是就绪状态,即具有抢夺CPU时间片(执行权)的能力,并不是直接执行
         * 这里刻意休眠100毫秒,让AA线程先去抢时间片,给AA一个先执行的优势
         */
        Thread.sleep(100);

        new Thread(() -> {
            try {
                phone.sendEmail();
                //phone.getHello();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"BB").start();

    }

}

分析以下八种情况的输出结果:

Case1:就上面的代码,直接执行

Case2:sendSMS()方法体加一行TimeUnit.SECONDS.sleep(4),即停留4秒

分析:对于上面两种情况,synchronized出现在示例方法中占的是对象锁,而两线程共用同一个对象,因此先抢时间片的线程AA先执行,而sleep是抱着锁睡,所以输出都是:

------sendSMS
------sendEmail

Case3:BB线程改为调用普通方法getHello

分析:getHello方法不用对象锁,所以不用等,而AA线程的sendSMS要sleep4秒,因此getHello就先输出:

------getHello
------sendSMS

Case4:两个手机Phone对象,分别给AA和BB线程调用两个synchronized方法

分析:两个Phone对象,两个对象锁,各自调synchronized实例方法,没有抢锁和等待的情况,没有sleep的自然先输出:

------sendEmail
------sendSMS

Case5:两个synchronized方法均加static改为静态方法,两线程共用1个Phone资源对象

Case6:两个synchronized方法均加static改为静态方法,两线程分别用2个Phone资源对象

分析:synchronized两个静态方法,锁的就是类锁,即当前类的Class对象,一个类就一把类锁,所以尽管有两个Phone对象在调也没用,先拿到类锁的先执行并输出:

------sendSMS
------sendEmail

Case7:BB线程调用静态同步sendEmail、AA线程调用无static的同步方法sendSMS,两线程共用1个Phone资源对象

Case8:BB线程调用静态同步sendEmail、AA线程调用无static的同步方法sendSMS,两线程分别用2个Phone资源对象

分析:不管1个/2个Phone对象,AA线程用的对象锁,BB线程用的类锁,互不影响,对象锁代码中有sleep,晚输出:

------sendEmail
------sendSMS

总结:

synchronized实现同步时:

  • synchronized加在静态方法上,锁的就是类锁,即当前类的Class对象,一个类就一把类锁
  • synchronized加在实例方法上,锁的是调用该方法的当前对象,是对象锁,一个类创建100个对象,就有100把对象锁
  • synchronized加在代码块上,锁的是synchronized括号里配置的对象
public class LockSyncDemo{

	Object object = new Object();

	public void m1(){
		synchronized (object) {
			System.out.println("同步代码块,锁object对象");
		}
	}

}

在这里插入图片描述

2、synchronized与monitor

写个简单Demo:

public class LockSyncDemo {

    Object object = new Object();

    public void m1(){
        synchronized (object){
            System.out.println("hello synchronized!");
        }
    }

    public static void main(String[] args) {

    }
}

编译运行后,在target中open in Terminal

在这里插入图片描述

在终端执行javap 对class文件反编译:

javap  -c  ***.class

//-c即反汇编,也可-v,即-verbose,输出更详细的附加信息

部分汇编指令如下:

在这里插入图片描述

上面发现,同步代码块上下出来一对monitor enter和exit,最后还有个未配对的monitor exit,这个类比finally中关资源,是为了万一同步代码块中间发生异常,也确保对象锁被释放。PS:并不总是多一个monitor exit,这个也可在上面Demo代码里的同步代码块中加一句异常:

//add
throw new RuntimeException("---exp");

再反编译,就只有一对堆monitor了

在这里插入图片描述

结论:synchronized同步代码块,底层是使用monitorenter和monitorexit指令。

再改为synchronized修饰实例方法:

public void m1(){
 	synchronized (object){
        System.out.println("hello synchronized!");
        
    }
}

在这里插入图片描述

结论:JVM读到ACC_SNCHRONIZED标志位,就知道这是一个同步方法。继续改为静态同步方法,再次汇编:

public static synchronized void m1() {
    System.out.println("hello synchronized!");
}

在这里插入图片描述

结论:ACC_STATIC,ACC_SYNCHRONIZED访问标志区分该方法是否为静态同步方法

3、管程Monitor

管程,Monitors,也称监视器,一种结构,结构内的共享资源对工作线程会互斥访问(独占,同一时间,用于保证只有一个线程可以访问被保护的数据或代码)。直白说就是锁。

  • JVM中同步的实现就是基于进入和退出监视器对象Monitor或者是管程对象的。每个对象实例都会有一个Monitor对象,Monitor对象也就是管程。
  • Monitor对象会和Java对象一同创建并销毁,底层由C++实现

比如前面同步方法的底层实现,当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先去持有monitor锁(执行线程就要求先成功持有管程),然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor。

4、Q:为什么每个Java对象都可以成为一个锁?

为什么任何一个对象都可以成为或者说带有一个锁(对象锁)?

在Java虚拟机HotSpot中,monitor由ObjectMonitor实现,

在这里插入图片描述

其对应在底层c++就是ObjectMonitor.cpp --> ObjectMonitor.hpp:

在这里插入图片描述

Java任何一个类都有Object这个父类,每个对象实例都会有一个Monitor对象,而Monitor对象的_owner中记录了哪个线程持有了ObjectMonitior对象锁。由此,每一个被锁的对象和Monitor对象关联,Monitor对象中又记录了当前持有锁的线程,当一个monitor对象被某个线程持有后,它便处于锁定状态,因此每个Java对象都可以成为一个锁。

总结:每个对象自带Monitor对象,而Monitor对象在c++里有属性owner,里面记录了当前谁持有锁,由此,每个Java对象都可以成为一个锁。

在这里插入图片描述

再补充下各个属性间的关系:

当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后,进入 _Owner 区域,并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器count加1。

若线程调用 wait() 方法,将释放当前持有的monitor,_owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。

若当前线程正常执行完毕也将释放monitor锁并复位变量的值,以便其他线程进入获取monitor锁。
在这里插入图片描述

5、小结

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-代号9527

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值