文章目录
- 1、synchronized
- 2、synchronized与monitor
- 3、管程Monitor
- 4、Q:为什么每个Java对象都可以成为一个锁?
- 5、小结
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、小结