多线程操作变量过程
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(或者栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存(物理内存),主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读写等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后把变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量拷贝副本,因此不同的线程间无法访问对方的工作内存,线程间的通信必须通过主内存来完成,简要的访问过程如下图:
JMM(Java内存模型)对于线程同步的要求
- 可见性
- 原子性
- 有序性
volatile的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
总结:volatile的禁止指令重排可以保证有序性,相较于JMM来说,除了原子性是硬伤外,另外两项,volatile都可以保证,所以说 volatile是低配版的synchronized,volatile是Java虚拟机提供的轻量级的同步机制。
保证可见性的证明:
可见性说明: 主内存中的变量修改,本地内存第一时间得到通知并修改本地内存中的值。
class MyDate {
int num = 0;
//变量增加1
public void numAddOne() {
++num;
}
}
public class T {
public static void main(String[] args) {
MyDate myDate = new MyDate();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3); //myThread线程休眠3秒
} catch (InterruptedException e) {
e.printStackTrace();
}
myDate.numAddOne();
System.out.println("current thread is:" + Thread.currentThread().getName() + ",num is:" + myDate.num);
}, "myThread").start();
//变量值为0时,线程阻塞在这里
while (myDate.num == 0) {
}
System.out.println("current thread is:" + Thread.currentThread().getName() + ",num is:" + myDate.num);
}
}
运行结果:
结果分析:myThread线程对变量num操作后,更新回主内存,但是main线程没有得到num值修改了的通知,导致main线程中的num还是0,所以程序阻塞在while循环处,无法打印后面的内容。
修改方案:在变量num前加上volatile,保证num的可见性,可以得到如下的运行结果:
不保证原子性的证明:
原子性说明: 一个操作可以分为多个步骤,原子性要求:只有当整个操作都完成,才停止,非原子性则是,在完整操作的某个中间步骤,能够被停止,等CPU再次唤醒,然后继续执行。
class MyDate {
volatile int num = 0;
//变量增加1
public void numAddOne() {
++num;
}
}
public class T {
public static void main(String[] args) {
MyDate myDate = new MyDate();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
myDate.numAddOne();
}
}, i + "").start();
}
//保证创建的20个线程运行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("compute finished, num is:" + myDate.num);
}
}
运行结果:
结果分析:因为volatile无法保证原子性,所以导致某个线程写步骤停止在某个步骤上,而其他线程用了该值,而停止的线程被唤醒后,继续使用该值,导致了写覆盖的情况,导致了最终的结果不是200000,当然也有可能会出现结果是200000的情况。
修改方案:可以在numAddOne方法前加上synchronized,但是仅仅为了一个加一的操作就使用synchronized,着实有一些杀鸡用牛刀的感觉。实际使用的方案:volatile无法保证原子性,那么我们就是用整型原子(AtomicInteger),整型原子当然能保证原子性了,如此一来就能保证原子性了。修改后的代码如下:
class MyDate {
volatile AtomicInteger num = new AtomicInteger();
//变量增加1
public void numAddOne() {
num.getAndIncrement();
}
}
public class T {
public static void main(String[] args) {
MyDate myDate = new MyDate();
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
myDate.numAddOne();
}
}, i + "").start();
}
//保证创建的20个线程运行完毕
while (Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println("compute finished, num is:" + myDate.num.get());
}
}
运行结果:
通过AtomicInteger和volatile我们就保证了上例子中多线程操作的原子性,可见性,有序性,和使用synchronized相比,AtomicInteger和volatile的效率要高很多,synchronized一次只允许一个线程操作,而AtomicInteger和volatile允许多个线程一起操作,效率自然也要高很多。
本篇文章没有对有序性进行描述,因为有序性需要汇编的支持,大意:在代码编译的过程中,编译器与处理器会对指令做重排,以提高效率。在单线程中指令重排并不会影响操作的结果,但是多线程无法保证结果准确,因此我们需要告诉编译器和处理器,我们的多线程程序不需要重排,通过volatile可以做到禁止指令重排。本篇中解决原子性问题使用的是 AtomicInteger,但是并没有介绍为什么AtomicInteger就能保证原子性,所以在以后的文章中,我会写一篇来介绍,为什么AtomicInteger就能保证原子性,以及它是如何做到保证原子性的。