并发三大特性
原子性
保证一段代码或者说一段逻辑,在一段时间内只有一个线程去执行,要么成功要么失败,这个期间不被其他线程所干扰。
从JMM角度来看问题,count++非原子操作场景,以下是JMM进行count++的流程:
1、count首先会从主内存中read操作读取出来。
2、count会被load操作读到工作副本中。
3、count会通过use操作交给执行引擎去操作。
4、最后赋值assign到工作副本中。
5、将工作副本中的count通过store转送至主内存。
6、通过write操作写入主内存
这一系列操作中每一个操作都是原子操作,但是并不保证整体原子性,当A线程将count值读取到工作副本后,发生上下文切换,在这个过程中B线程执行完count++,就破坏了操作的原子性,这是就需要通过加锁来解决操作的原子性问题,当线程进行count++时进行加锁,其他线程无法进行破坏,也可以通过cas自旋锁来保证整体操作的原子性。
有序性
public class Test{
private static Integer number=0;
private static boolean flag=false;
public void a(){
number=2;
flag=true;
}
public void b(){
if(flag){
number=number*number;
}
}
public void c(){
a();
b();
}
}
上述代码中,执行c方法,首先会执行a方法,然后执行b方法,但是在进行指令重排后可能在执行c方法后先会执行b方法,然后执行a方法,这就导致了b方法的异常执行,而volatile、内存屏障、synchronized方法以及synchronized代码块中的代码可以完成禁止指令重排。
public class Test{
private static Integer number=0;
private static boolean flag=false;
public void a(){
number=2;
flag=true;
}
public void b(){
if(flag){
number=number*number;
}
}
public void c(){
//指令重排
b();
a();
}
}
可见性
在JMM内存模型中划分为工作内存以及主内存,CPU执行线程然后获取主内存中的数据,并存储到工作内存中进行操作,A、B两个线程同时操作count++,A线程因为业务复杂度或其他缘故仅仅只是将count加载到了工作内存中并没有进一步的修改操作,B线程将count值修改为1后,A线程的数据仍然为0,不管1是在B线程的工作内存中还是主内存中,A线程都没有办法感知到。
并发三大问题
同步
同步是指线程与线程之间如何协作,并发中解决同步问题可通过Countdownlatch、Semaphore、CyclicBarrier解决。
分工
分工是指任务如何高效的进行分解执行,可通过线程池来决绝。
互斥
互斥是保证一个共享资源,在某一时刻只能有一个线程持有,可通过同步锁、lock、cas等方式解决。
volatile 如何保证可见性的,又是如何保证有序性的
1、从汇编的角度来讲在读写volatile变量时会在指令前加一个lock#前缀,通过lock前缀指令,每个处理器都有嗅探功能,MESI一致性协议基于嗅探功能完成缓存一致性。
2、读写volatile变量的前后,lock#前缀指令在CPU执行时会隐式的实现内存屏障的效果,在读之前设定了loadload读之后设定了loadstore,在写之前设定了storestore在写之后设定了storeload,
3、从JMM角度来看,read出volatile变量后然后load到工作副本,然后通过use将变量交给执行引擎去操作,操作完后赋值给工作副本,然后store发送给主内存,通过write写入主内存。就会导致其他的线程工作副本该volatile变量失效,等到其他的线程去获取这个变量时就会直接去获取主内存的数据。
什么是重排序,为什么要禁止重排序
重排序大致可分为两类,编译器的重排序以及处理器级别的重排序,不管是编译器级别的还是处理器级别的它们都是以执行优化为目的从而进行指令的重排,如下所示:
public class test{
int a,b,c;
public void a(){
a=1;
b=2;
c=a;
}
}
什么是重排序,为什么要禁止重排序
正常情况下,a=1、b=2、c=a执行是没问题的,站在编译器角度而言,个人认为在JMM模型中a=1这个1首先会从主内存通过read、load指令去获取并写入工作内存中,然后赋值给a,然后写入工作内存再然后的写入主内存中,然后读取这个2去赋值给b并写入主内存中,但是编译器发现之前被赋值的a在后面赋值给c,要知道工作内存中的数据是有失效时间的,若一段时间内a变量没有被操作就会失效,而且这个时间是很短的大约100ms左右,若要进行c的赋值时a的数据在工作副本中失效了,那么就得重新从主内存读取,但是如果将代码的执行顺序变更为a=1、c=a、b=2,这样的话就减少了一次读取主内存的操作了,这就是重排序的目的。
那为什么要禁止重排序呢?
看以下示例
public class Test{
private static int a,b,c,d;
public static void a(){
a=c;
d=a;
}
public static void b(){
b=1;
c=b;
}
public static void main(String [] args){
//线程1
new Thread(){
public void run(){
Test.b();
}
}.start();
//线程2
new Thread(){
public void run(){
Test.a();
}
}.start();
}
}
为了达到a,b,c,d全部都是1的目的时必须先执行线程1然后执行线程2,但是通过指令重排后先就执行的线程2然后执行的线程1,导致最后结果出现偏差b、c为1,a、d为0。在并发场景下,指令重排序给代码执行带来了太多不确定性,导致有些问题无法定位,内存屏障就是为了解决指令重排序的。
什么是并发什么是并行
并发:可以理解为单核CPU同时要处理多个事件,每一个事件的执行都需要去获取CPU时间片,然后交替执行。
并行:可以理解为多核CPU同时执行多个事件。