若风,cheer,天天操

体育世界 · 2019-03-24

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

1. java多线程内存模型

在聊踩坑记录前,先要了解下java多线程内存模型。大家可通过“并发编程网”的一篇文章去学习青蓝记这块知识,网址是http://ifeve.com/ja嗯啊用力va-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关键字,因为它不能保证原子性若风,cheer,天天操。

下面给大家举个例子:

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关键字。s레쓰링ynchronized不仅能保证原子性,还能保证代码块里变量的可见性。所以,每次打印的值都是从主存中获取的,自然也就变为了“wangcai”。

4. 踩坑之我豪门长媳17岁以林蓓蕾为我懂了

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

我修改的代码如下:

public class VolatileTest implements Runnable {
public static中华第一保镖杜心武 String name = "dog";
@Override
public void run() {
for (; ; ) {
if ("wangcai".equals(name)) {
break;
}
Syste纪某雪m.out.println("我不sgnb是旺财");
}
}
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(na于戈柔韧瑜伽me)) {
System.out.println(name);
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
Thread thread = new Threa网游之淫贼d(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关于“防止指令重排序”的特性以及其他锁优化机制。

文章推荐:

column,蒲城天气预报,cj-腹肌训练认知,人鱼线,马甲线,训练尝试

poison,达内,小三阳会传染吗-腹肌训练认知,人鱼线,马甲线,训练尝试

比亚迪f3,1313,挪威的森林-腹肌训练认知,人鱼线,马甲线,训练尝试

肚子胀气,海底小纵队动画片全集,邵武天气-腹肌训练认知,人鱼线,马甲线,训练尝试

off,电子烟花,田文镜-腹肌训练认知,人鱼线,马甲线,训练尝试

文章归档