深入详细了解synchronized底层原理

    欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。


    在多线程之间,共享变量的值是线程不安全的,因为线程在开始运行之后都会拥有自己的工作空间,而从自己工作空间把修改的值刷新回主存的时候需要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”是为了线程遇到异常之后释放锁而准备的。
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值