关于Synchronized
synchronized 是 Java 中的一个关键字,它是一个重量级锁,用于保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,同时也可以保证可见性,即一个线程的变化可以被其他线程所见
有三种应用:
/**
* 8种案例说明
* 1.标准访问有ab两个线程,请问先打印邮件还是短信 //只有一个对象 a拿到先锁住了整个对象
* 2.sendEmail方法暂停3秒钟,请问先打印邮件还是短信
* 3.新增一个普通的heLlo方法,请问先打印邮件还是heLLo 回答错误了 不加synchronized跟资源是否被锁没关系
* 4.有两部手机,请问先打印邮件还是短信
* 5.两个静态同步方法,同1部手机,请问先打印邮件还是短信
* 6.两个静态同步方法,2部手机,请问先打印邮件还是短信
* 7.1个静态同步方法,1个普通同步方法,同1部手机,请问先打印邮件还是短信 (回答错误了 静态同步方法锁类 普通方法锁对象 并不冲突)
* 8.1个静态同步方法,1个普通同步方法,2部手机,请问先打印邮件还是短信
*
*
* * * 1-2
* * * * 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,
* * * * 其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一的一个线程去访问这些synchronized方法
* * * * 锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
* * 3-4
* * * 加个普通方法后发现和同步锁无关,hello
* * * 换成两个对象后,不是同一把锁了,情况立刻变化。
* *
* * *
* * * 5-6 都换成静态同步方法后,情况又变化
* * * 三种 synchronized 锁的内容有一些差别:
* * * 对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁——实例对象本身,
* * * 对于静态同步方法,锁的是当前类的Class对象,如Phone.class唯一的一个模板
* * * 对于同步方法块,锁的是 synchronized 括号内的对象
* *
* * * 7-8
* * * 当一个线程试图访问同步代码时它首先必须得到锁,退出或抛出异常时必须释放锁。
* * * *
* * * * 所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this
* * * * 也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放锁后才能获取锁。
* * * *
* * * * 所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板Class
* * * * 具体实例对象this和唯一模板Class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的
* * * * 但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。
*/
class Phone{
public synchronized void sendEmail(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("sendEmail");
}
public synchronized void sendSms(){
System.out.println("sendSMS");
}
}
public class test01 {
public static void main(String[] args) {
Phone phone = new Phone();
Phone phone1 = new Phone();
new Thread(()->{
phone.sendEmail();
},"a").start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
}
new Thread(()->{
phone.sendSms();
},"b").start();
}
}
1 对象锁(monitor)机制
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {
System.out.println("hello synchronized!");
}
}
}
上述代码通过synchronized“锁住”当前类对象来进行同步,将java代码进行编译之后通过javap -v SynchronizedDemo .class来查看对应的main方法字节码如下:
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class com/codercc/chapter3/SynchronizedDemo
2: dup
3: astore_1
4: **monitorenter**
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String hello synchronized!
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit
15: **goto** 23
18: astore_2
19: aload_1
20: **monitorexit**
21: aload_2
22: **athrow**
23: **return
我们可以看到一个monitorenter对应两个monitorexit
monitorenter指令获取到对象的monitor(也通常称之为对象锁)后才能往下进行执行,在处理完对应的方法内部逻辑之后通过monitorexit指令来释放所持有的monitor,以供其他并发实体进行获取。代码后续执行到第15行goto语句进而继续到第23行return指令,方法成功执行退出。另外当方法异常的情况下,如果monitor不进行释放,对其他阻塞对待的并发实体来说就一直没有机会获取到了,系统会形成死锁状态很显然这样是不合理。
因此针对异常的情况,会执行到第20行指令通过monitorexit释放monitor锁,进一步通过第22行字节码athrow抛出对应的异常。从字节码指令分析也可以看出在使用synchronized是具备隐式加锁和释放锁的操作便利性的,并且针对异常情况也做了释放锁的处理。
每个对象都存在一个与之关联的monitor,线程对monitor持有的方式以及持有时机决定了synchronized的锁状态以及synchronized的状态升级方式。monitor是通过C++中ObjectMonitor实现
从ObjectMonitor的结构中可以看出主要维护WaitSet以及EntryList两个队列来保存ObjectWaiter 对象,当每个阻塞等待获取锁的线程都会被封装成ObjectWaiter对象来进行入队,与此同时如果获取到锁资源的话就会出队操作。另外_owner则指向当前持有ObjectMonitor对象的线程。等待获取锁以及获取锁出队的示意图如下图所示:
当多个线程进行获取锁的时候,首先都会进行_EntryList队列,其中一个线程获取到对象的monitor后,对monitor而言就会将_owner变量设置为当前线程,并且monitor维护的计数器就会加1。如果当前线程执行完逻辑并退出后,monitor中_owner变量就会清空并且计数器减1,这样就能让其他线程能够竞争到monitor。另外,如果调用了wait()方法后,当前线程就会进入到_WaitSet中等待被唤醒,如果被唤醒并且执行退出后,也会对状态量进行重置,也便于其他线程能够获取到monitor。
从线程状态变化的角度来看,如果要想进入到同步块或者执行同步方法,都需要先获取到对象的monitor,如果获取不到则会变更为BLOCKED状态,具体过程如下图所示:
从上图可以看出任意线程对Object的访问,首先要获得Object的monitor,如果获取失败,该线程就会进入到同步队列中,线程状态变为BLOCKED。当monitor持有者释放后,在同步队列中的线程才会有机会重新获取monitor,才能继续执行。