通过踩坑带你读透虚拟机的“锁粗化”

之前在学习volatile时,踩过一些坑。通过这些坑,学习了一些jvm的锁优化机制。后来在面试的过程中,被问到的概率还挺高。于是,我整理了这篇踩坑记录。

1. java多线程内存模型

在聊踩坑记录前,先要了解下java多线程内存模型。大家可通过“并发编程网”的一篇文章去学习这块知识,网址是http://ifeve.com/java-memory-model-1/。下面截取部分段落,先让大家熟悉下。

在java中,所有实例域、静态域和数组元素存储在堆内存中,堆内存在线程之间共享(本文使用“共享变量”这个术语代指实例域,静态域和数组元素)。

局部变量(Local variables),方法定义参数(java语言规范称之为formal method parameters)和异常处理器参数(exception handler parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。

Java内存模型的抽象示意图如下:

多线程内存模型

从上图来看,线程A与线程B之间如要通信的话,必须要经历下面2个步骤:

1、首先,线程A把本地内存A中更新过的共享变量副本刷新到主内存中去。

2、然后,线程B到主内存中去读取线程A之前已更新过的共享变量。

上面内容可以总结如下:

1、多线程在运行时,会有主内存和工作内存的区分。
2、每个线程都有各自的工作内存,工作内存会复制一份主内存的变量副本。
3、线程其后的运行,都是修改工作内存中的变量副本。然后在某个时间,再同步到主存中。
4、这种工作机制,可能使得多个线程在同一个时刻获取到的变量值不同。

2. volatile关键字的作用

2.1. volatile关键字语义

共享变量被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

2.2. volatile关键字如何保证线程间的可见性?

1、使用volatile关键字,线程会将修改的值立即同步至主内存中

2、使用volatile关键字,线程会强制从主存中读取值。

3、所以,这就保证了某个线程修改的值,会立即被其余线程获得。

2.3. volatile关键字不保证原子性

volatile并不能代替synchronized关键字,因为它不能保证原子性。

下面给大家举个例子:

1、多个线程对变量i进行自增操作。
2、A线程从主存中获得变量i的值,为6.
3、在A获取主存的值后,B线程将运算结果7同步至主存。
4、A线程对变量i进行i++操作,然后同步至主存。主存结果依然为7。这时i++明显小于预期结果。

造成上述原因,就是因为volatile关键字不能保证自增操作的原子性。

3. 踩坑之synchronized的可见性

看完java多线程模型和volatile关键字的作用,我们正式来聊踩坑记录。

public class VolatileTest implements Runnable {
    public static String name = "dog";

    @Override
    public void run() {
        while (true) {
            System.out.println(name);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        Thread thread = new Thread(volatileTest);
        thread.start();
        // 让主线程睡一段时间,保证子线程的开启。
        Thread.sleep(5000);
        VolatileTest.name = "wangcai";
    }
}

上述的name字段,我并没有加volatile关键字。我还调用了Thread.sleep(5000);,以便让子线程先开启。

按照多线程模型的描述,子线程里的name字段应该是拷贝的变量副本“dog”。所以我在主线程修改name值为“wangcai”,并不对子线程可见。所以,按理来说,应无限循环打印“dog”。但事实上,打印结果如下:

dog
dog
dog
wangcai
wangcai
wangcai

这和上面的原理不符啊,一度让我十分困惑。后来我翻了下System.out.println的源码,发现其源码如下:

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

看到源码,答案也就呼之欲出了。因为println方法添加了synchronized关键字。synchronized不仅能保证原子性,还能保证代码块里变量的可见性。所以,每次打印的值都是从主存中获取的,自然也就变为了“wangcai”。

4. 踩坑之我以为我懂了

发现上述原因后,我决定不再用System.out.println打印变量,这样就不会触发从主存中读取数据。然而我还是太天真,事情的发展就是这么曲折。

我修改的代码如下:

public class VolatileTest implements Runnable {
    public static String name = "dog";

    @Override
    public void run() {
        for (; ; ) {
            if ("wangcai".equals(name)) {
                break;
            }
            System.out.println("我不是旺财");
        }

    }

    public static void main(String[] args) throws InterruptedException {
        VolatileTest volatileTest = new VolatileTest();
        Thread thread = new Thread(volatileTest);
        thread.start();
        Thread.sleep(5000);
        VolatileTest.name = "wangcai";
    }
}

这次我仍然没有添加volatile关键字,更没有打印name变量。按理说,这次应该无限循环打印“我不是旺财”了吧。但是线程跳出循环,并停止了。这时,我已经开始对多线程模型产生动摇了。经过探索,我又知道了“锁粗化”的概念。

5. 锁粗化

下面,我们看看《深入理解java虚拟机》对锁粗化的描述:

原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小-只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。

大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

如果虚拟机探测到有这样零碎的操作都对统一对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。

将原代码生成的class文件进行反编译,得到如下代码:

public void run() {
    while(!"wangcai".equals(name)) {
        System.out.println("我不是旺财");
    }
}

于是,while循环里的System.out.println("我不是旺财");具有同步代码块,每次都对PrintStream加锁。于是,经过虚拟机的锁粗化,锁扩展到了外部,可见性也扩展到了外部。所以子线程能看见主线程对name的改变,所以会让线程跳出,并停止。

6. 守得云开见月明

public class Test implements Runnable {

    private static String name = "dog";

    @Override
    public void run() {
        while (true) {
            if ("wangcai".equals(name)) {
                System.out.println(name);
                break;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Test test = new Test();
        Thread thread = new Thread(test);
        thread.start();
        Thread.sleep(5000);
        Test.name = "wangcai";
    }
}

最终,将代码改成如上的样式。不加volatile,主线程对name的改变,子线程不可见。所以线程会一直循环,不退出。

加了volatile,主线程的对name的改变,子线程是可见的。所以会打出“wangcai”,并退出。

看到这里,如果你有某些疑问,我会觉得你好好研读上面的内容了。在while循环快中,我也加入了System.out.println函数,为什么没有进行锁粗化?这个依然是由反编译后的代码来决定的:

public void run() {
    while(!"wangcai".equals(name)) {
        ;
    }

    System.out.println("我是旺财");
}

通过反编译得到的源码,我们发现虚拟机对第二个代码进行了优化,是将System.out.println("我是旺财");放在循环外的。而第一个优化后的代码,是将System.out.println("我不是旺财");放在循环里的。

所以,第二个不会进行锁粗化,而第一个会进行锁粗化。

7. 总结

上面就是我在学习volatile关键字时,遇到的各种坑。但是通过踩坑,我不仅更加深入了解了volatile关键字,我也学会了虚拟机的锁粗化机制。虽然我一开始是茫然的,但是我没有放弃思考。每一次的难题,都会让我弥补知识上的短板。走出自己的知识舒适区,你才能收获成长。

通过实战,你会更为扎实地掌握所学知识点。面试的时候,通过代码向面试官阐述自己的思考过程,更能凸显出你将理论融入实践的能力,而不只是“纸上谈兵”。

后面有机会,我还会和大家分享volatile关于“防止指令重排序”的特性以及其他锁优化机制。

还是那句话,愿我们共同进步!

作者:永不言Qi
QQ: 591232672
e-mail:591232672@qq.com
版权声明:转载请保留此链接,不得用于商业用途。
虽然我不是最优秀的程序员,但我还是想尽自己最大的努力,去分享一些学习心得。
如有错误,欢迎指正。若有幸能博得您的喜爱,欢迎关注及点赞哦。
愿我们共同进步!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容