JDK源码系列:synchronized与wait、notify、notifyAll

            大家好,今天聊一聊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中,如下示意图:

bfce374c0306bfab4e3f8122e288d9f5.png

这个竞争锁的过程是怎么完成的呢?

是靠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 方法共用了一把锁;

a78c2826d93cf5715e219b0d1089b42a.png

2)针对不同的SyncDemo对象来说,是可以同时被进入的,原因就是每个对象有且仅有一把锁,每个对象的锁是独立的,互不干涉的;

158665dd15161eb96672588cf9bfd988.png

案例3:

public class SyncDemo {
    public static synchronized int add(int a,int b){
        return a+b;
    }
}

静态方法上加synchronized,这时候加的是一把类锁(SyncDemo.class,这是一把全局锁,只有一把),那么这把锁存储在什么位置?

存在于元数据区的Class对象的markword中,如下图:

291f655a28669e431c8151ade2ef1bb8.png

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中的代码执行过程:

45088f8767695971af029d0054f98547.png

460b9616a10751f3a674c3704c518dd6.png

dd69334245ac5ca3768dfcfd3bb452d9.png

5dd7ba1f49c545664928c1478a635791.png

e780a59897c0ed7d8da95a1de9864a13.png

b967ae1ed1b374b1685cf39885a3cb26.png

80a0d4309191e63015ef3078c4ddd03a.png

就这样:

当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是用来获取对象锁或者类锁的一种机制,更基础更纯粹,所以后者需要前者的配合才能正常使用,也就是说后者依赖于前者,前者并不依赖于后者。

欢迎关注我的公众号:“老吕架构”        

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吕哥架构

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

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

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

打赏作者

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

抵扣说明:

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

余额充值