在博问中遇到一个多线程的问题:提问者问到volatile
然后发现一个多线程解释非常好的博客,下面链接
https://www.cnblogs.com/dolphin0520/p/3920373.html
博客理解:
我们在编写代码的时候有时候会用到多线程:
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题
1.原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
简单理解就是事件要么全执行要么全不执行
2.可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
简单理解多个线程之间要相互看到对方的操作,保证自己对同一个数据操作不会受到影响
3.有序性:即程序执行的顺序按照代码的先后顺序执行。
代码是按一定顺序排列的,然而有时会发生指令重排序
指令重排序:一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
注意:处理器在进行重排序时是会考虑指令之间的数据依赖性
单线程排序:处理器在进行重排序时是会考虑指令之间的数据依赖性,单线程不会代码顺序执行出现错误问题
多线程:
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子: //线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context); 上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,
而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。 也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确
内存模型
1.原子性:
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
2.可见性:
对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
3.有序性:
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。 在Java里面,可以通过volatile关键字来保证一定的“有序性”(具体原理在下一节讲述)。另外可以通过synchronized和Lock来保证有序性,
很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。 另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。
如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。 下面就来具体介绍下happens-before原则(先行发生原则): 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作 volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始 这8条原则摘自《深入理解Java虚拟机》。
下面谈一下:volatile 按顺序理解以下几点:
1.(多线程中运行)在多核CPU中,每条线程可能运行于不同的CPU中,因此每个线程运行时有自己的高速缓存(对单核CPU来说,其实也会出现这种问题,只不过是以线程调度的形式来分别执行的------要记住每一个线程都有自己的域,即是自己的高速缓存
2.volatile 使用作用:
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 2)禁止进行指令重排序。
3.volatile使用注意
通常来说,使用volatile必须具备以下2个条件: 1)对变量的写操作不依赖于当前值 2)该变量没有包含在具有其他变量的不变式中 实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。 事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行
博问提问:下面第二段代码不执行步骤A ,而
static int initVal = 0; 改成 static volatile int initVal=0就可访问到A步骤
为什么?
当加入 volatile 时,新值对其他线程来说是立即可见的。A步骤被执行
当不加volatile 时,新值不是立即可见,initVal改变,另一个线程还是用老值(自己缓存线程的值),还有一种解决让另一个线程休眠一秒
如在
new Thread(() -> { int localVal = initVal; while (localVal < MAX) { try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } if (initVal != localVal) { b++; System.out.printf("The iniVal is updated to [%d]\n", initVal);// A步骤 localVal = initVal; } } }, "Reader").start();
public class VolatileDemo2 { final static int MAX = 5; static int initVal = 0; static volatile int b=0; public static void main(String[] args) { new Thread(() -> { int localVal = initVal; while (localVal < MAX) { System.out.printf("The initVal will be cahnged to [%d]\n", ++localVal); initVal = localVal; try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } }, "Updater").start(); new Thread(() -> { int localVal = initVal; while (localVal < MAX) { if (initVal != localVal) { b++; System.out.printf("The iniVal is updated to [%d]\n", initVal);// A步骤 localVal = initVal; } } }, "Reader").start(); } }