大家好,今天聊一聊synchronized与obj.wait()、obj.notify()、obj.notifyAll() 之间的关系以及它们的实现原理。
我们今天采用边写demo边分析的方式来进行。
案例1:
public class SyncDemo {
public synchronized int add(int a,int b){
return a+b;
}
}
在这个demo中 调用方要想进入add方法,必须先获取一把锁(对象锁,每个对象都有且只有一把),这把锁的位置存在于为对象分配的内存空间的Markword中,如下示意图:
这个竞争锁的过程是怎么完成的呢?
是靠JVM来管控的,它有自己的策略,目前的策略大致是会根据竞争的激烈程度,先后使用“偏向锁”、“轻量级锁”、“重量级锁”来优化应对各种场景。
案例2:
public class SyncDemo {
public synchronized int add(int a,int b){
return a+b;
}
public synchronized int sub(int a,int b){
return a-b;
}
}
问题1:add 和sub方法能不能被同时被两个线程进入?
答案是:
1)针对同一个SyncDemo对象来说,是不能同时被进入的,原因就是 add和sub 方法共用了一把锁;
2)针对不同的SyncDemo对象来说,是可以同时被进入的,原因就是每个对象有且仅有一把锁,每个对象的锁是独立的,互不干涉的;
案例3:
public class SyncDemo {
public static synchronized int add(int a,int b){
return a+b;
}
}
静态方法上加synchronized,这时候加的是一把类锁(SyncDemo.class,这是一把全局锁,只有一把),那么这把锁存储在什么位置?
存在于元数据区的Class对象的markword中,如下图:
Class类是表示类结构的类,反射编程中的类结构信息正是来自于此处,它的父类也是Object,所以它的实例应该也具有 普通类实例的特征,但是注意内存的位置是 元数据区,普通对象是位于堆内存中的,这是和普通对象的区别。
案例4:
public class SyncDemo {
public static synchronized int add(int a,int b){
return a+b;
}
public static synchronized int sub(int a,int b){
return a-b;
}
}
1)因为两个方法共享了一把类锁,所以 add方法和sub方法是不能被同时进入的,这是和案例2的相似之处。
2)类锁只有一把(每个类只会被加载一次,形成一个Class对象),是全局的,和 SyncDemo对象数量无关,这是和案例2的不同之处。
案例5:
public class SyncDemo {
public int add(int a,int b){
int c ;
synchronized(this){
c = a+b;
}
return c;
}
}
和案例1原理一致
案例6:
public class SyncDemo {
public int add(int a,int b){
int c ;
synchronized(SyncDemo.class){
c = a+b;
}
return c;
}
}
和案例3原理一致
案例7:
public class SyncDemo {
public synchronized int add(int a,int b){
return a+b;
}
public static synchronized int sub(int a,int b){
return a-b;
}
}
或者如下
public class SyncDemo {
public int add(int a,int b){
int c ;
synchronized(this){
c = a+b;
}
return c;
}
public int sub(int a,int b){
int c ;
synchronized(SyncDemo.class){
c = a-b;
}
return c;
}
}
问题 add 和sub 能不能同时进入?
由上面案例的分析可以知道,add和sub各自都有一把锁,互不干涉,所以是可以同时被进入的。
案例8、
实现两个线程交替调用add和sub方法
public class SyncDemo {
private volatile int flag = 1;
@SneakyThrows
public synchronized int add(int a, int b){
while (flag!=1){
this.notifyAll();
this.wait();
}
flag = 2;
return a+b;
}
@SneakyThrows
public synchronized int sub(int a, int b){
while (flag!=2){
this.notifyAll();
this.wait();
}
flag = 1;
return a-b;
}
}
public static void main(String[] args) {
final SyncDemo syncDemo = new SyncDemo();
ThreadFactory threadFactory = Executors.defaultThreadFactory();
threadFactory.newThread(()->{
while (true){
System.out.println(syncDemo.add(2,1));
}
}).start();
threadFactory.newThread(()->{
while (true){
System.out.println(syncDemo.sub(2,1));
}
}).start();
while (true);
}
这里面涉及了 Object类中的 wait、notify、notifyAll 方法的原理;
看下Object类中方法的定义:
public final native void wait(long timeout) throws InterruptedException;
public final native void notify();
public final native void notifyAll();
通过方法的定义可以得出如下结论:
1)这三个方法是实例方法,不能用在static方法或者静态代码块中
2)也就是说使用这三个方法时的锁需要是对象锁,而不能是类锁
由上面的结论就推出下面的结论:
1)这三个方法和synchronized进行配合使用时,synchronized竞争的那把锁只能是 一把对象锁,不能是类锁;
2)这三个方法必须在synchronized代码的环绕下使用,为什么不能脱离?因为这三个方法执行的前提是需要一把锁,否则可能会存在线程安全问题(这是JVM内部实现,我猜的),而synchronized的角色就是保证获取这把锁,否则就别想进去。
3)在编译层面也体现了这个原则,如果没有synchronized的环绕来使用wait,编译都过不去。
用示意图来表示下案例8中的代码执行过程:
就这样:
当A线程不满足执行条件时则通知(notifyAll) B线程,同时自己进入等待队列(wait),并释放那把锁;
当B线程不满足执行条件时则通知 (notifyAll) A线程,同时自己进入等待队列(wait),并释放那把锁;
如此循环下去,就可以实现交替执行add和sub方法。
这就是实现了 线程之间的协作。
总结
通过上面的案例分析,得到如下知识:
1)在Java中每个对象或者实例 都有一把对象锁,这把锁的位置位于 堆内存中对象头的markword中,每产生一个对象,就伴随一把锁的诞生;
2)每个Class对象(存放类的元数据信息,反射编程中的类结构信息正是来源于此)都有一把类锁,这把锁的位置位于 元数据区中Class对象头的markword中(没有经过源码确认,根据对象原理推断的),类锁和类型一对一,JVM每加载一个新的类型,意味着一把类锁的诞生;
3)synchronized可以配合任何对象锁或者类锁 来使用,实例方法用的是当前对象锁,静态方法用的是当前类锁;
4)在更小粒度的代码块中可以按你的意愿去指定任何对象锁或者类锁;
5)synchronized锁的获取和释放过程由JVM来管控;
6)wait、notify、notifyAll是Object类的实例方法,这意味着不能用在synchronized修饰的静态方法里,也不能用在synchronized修饰的静态代码块中,只能配合对象锁使用,不能配合类锁使用;
7)如果wait、notify、notifyAll方法没有被synchronized环绕,在编译阶段就会报错,如果被环绕但是用的锁不一致,则在运行阶段报错(IllegalMonitorStateException);
8) obj.wait() 是将当前线程放入obj的等待队列中,并释放当前持有的锁;
9) obj.notify()会从obj的等待队列中唤醒一个线程(具体哪一个不确定,不同JDK版本策略可能不同)
10)obj.notifyAll会从obj的等待队列中唤醒所有线程(推荐)
11)退出等待队列的方法:
1)有其它线程调用了 obj的notify方法
2)有其它线程调用了obj的notifyAll方法
3)有其它线程对等待的线程调用了 interrupt 方法
4)wait方法超时
12) synchronized和wait、notify、notifyAll的关系,后者是Object的实例方法,是实现线程之间协作的一种机制,必须在拥有当前对象锁的情况下才能被调用,而前者synchronized是用来获取对象锁或者类锁的一种机制,更基础更纯粹,所以后者需要前者的配合才能正常使用,也就是说后者依赖于前者,前者并不依赖于后者。
欢迎关注我的公众号:“老吕架构”