关于java:深入详细了解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”是为了线程遇到异样之后开释锁而筹备的。

    欢送大家搜寻“小猴子的技术笔记”关注我的公众号,有问题能够及时和我交换。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理