synchronized为什么会有两个monitor exit

在多线程之间,共享变量的值是线程不安全的,因为线程在开始运行之后都会拥有自己的工作空间,而从自己工作空间把修改的值刷新回主存的时候需要CPU的调度。因此,一个线程看到的变量可能并不是最新的。

我们假设有个Share类中存放了一个共享的变量“count”。

public class Share {    public int count = 10000;    public void decrement() {        count--;    }    public int getCount() {        return count;    }}

然后有两个线程可以对这个共享的变量进行操作,每个线程都调用了5000次“decrement()”方法类进行共享变量的值修改:

public class ShareThread01 implements Runnable{    private Share share;    public AccountThread01(Share share) {        this.share = share;    }    @Override    public void run() {        for (int i = 0; i < 5000; i++) {            share.decrement();        }    }}
public class ShareThread02 implements Runnable{    private Share share;    public AccountThread02(Share share) {        this.share = share;    }    @Override    public void run() {        for (int i = 0; i < 5000; i++) {            share.decrement();        }    }}

如果上面的代码按照预期的执行,那么最后的结果应该是0。请执行下面的代码进行验证:public class ShareTest {

    public static void main(String[] args) throws InterruptedException {       while(true) {           Share share = new Share();           Thread t1 = new Thread(new ShareThread01(share));           Thread t2 = new Thread(new ShareThread02(share));           t1.start();           t2.start();           TimeUnit.SECONDS.sleep(2);           System.out.println(share.getCount());       }    }}

运行上面的程序,你会发现每次输出的结果是不一样的。因为文章开头已经说过,这是由于Java在多个线程同时访问同一个对象的成员变量的时候,每个线程都拥有了这个对象变量的拷贝。因此在程序执行的过程中,一个线程所看到的变量并不一定是最新的

也许你想到了使用之前学习的“volatile”关键字来使共享变量进行内存可见,保证线程安全。于是上述程序改成了如下:

public class ShareThread02 implements Runnable{    private volatile Share share;    public ShareThread02(Share share) {        this.share = share;    }    @Override    public void run() {        for (int i = 0; i < 5000; i++) {            share.decrement();        }    }}

然后再次运行测试程序也会发现,所输出的结果并不是你想要的结果。请记住:关键字“volatile”只是保证了多线程之间共享变量的内存可见性,它并不保证共享变量的原子性。

这个时候关键字“synchronized”就派上了用场。它可以保证,同一个时刻只有一个线程能够访问被“synchronized”修饰的方法或者代码块。将上述代码改成下面这样:


public class Share {
    public int count = 10000;
    public synchronized void decrement() {
        count--;
    }
    public int getCount() {
        return count;
    }
}

 

再次运行测试程序,这个时候你会发现,每次得到的结果都是一样的。那么为什么添加了关键字“synchronized”之后就能够按照预期的进行执行呢?

通过执行“javap -v Share.class”来看看底层做了哪些修饰:

可以看到,在方法上进行了关键字”synchronized“的修饰,底层的实现是标记了一个"ACC_SYNCHRONIZED"的标识。代码如果遇到了这个标识,就表示获取到了对象的监视器monitor(monitor对象是由C++实现的),这个获取的过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

除此之外,关键字”synchronized“还可以对代码块进行加锁:


public class Share {
    public int count = 10000;
    public void decrement() {
        synchronized (Share.class) {
            count--;
        }
    }
    public int getCount() {
        return count;
    }
}

将上述代码执行“javap -v Share.class”反编译之后,可以看到如下:

”synchronized“关键字锁代码块的时候他提供了“monitor enter”“monitor exit”两个JVM指令,它能够保证任何时候线程执行到monitor enter成功之前都必须从主内存中获取数据。“monitor exit”退出之后,共享变量被更新后的值刷新到主内存中,因此”synchronized“关键字还可以保证内存的可见性。

如果你仔细观察就会发现,15和21有两个“monitor exit”那么为什么会有两个“monitor exit”呢?我们做这样一个假设:如果线程启动执行的过程中突然遇到异常了,这个时候线程该怎么办呢?总不能一直持有锁吧!于是线程就会释放锁。因此,第二个“monitor exit”是为了线程遇到异常之后释放锁而准备的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值