并发编程三要素
-
原子性
一个或多个操作要么全部执行成功要么全部执行失败,期间不中断,不存在上下文切换,线程切换会带来原子性问题
int a=1;是原子操作
而num++;不是原子操作
解决:
-
使用synchronize或Lock(ReentrantLock)
public class LockTest { private int a=0; Lock lock=new ReentrantLock(); //使用lock,每个对象都是有锁,只有获得锁才能进行对应操作 public void add(){ lock.lock(); try { a++; }finally { lock.unlock(); } } //锁的是整个方法 public synchronized void add2(){ a++; } }
思想:把一个方法或代码看做一个整体,保证是一个不可分割的整体。
-
-
可见性
见下面volatile的分析
-
有序性
程序的执行按代码顺序执行,jvm或cpu可能为了优化运行效率对代码进行重排序,但不会影响效果。但在多线程环境下就会出现问题。
volatile
解释
volatile关键字和synchronize的区别
-
volatile是轻量级的synchronize,保证了共享变量的可见性,如果值发生了变化,其他线程会立刻感知到,避免了脏读的现象,还能保证有序性,但不能保证原子性
能够禁止指令重排
-
synchronize保证了可见性,也保证原子性(优化越来越多)。
其有序性是由“一个变量同一时刻只允许一条线程对其进行lock操作”这条规则获取
可见性
指一个线程修改了共享变量后,其他线程可以立马感知到,这也就可以避免脏读的发生。
如线程A将x修改为1后,没有立即写回到主内存,而线程B仍然使用旧值0,但当x被volatile修饰后,修改的变量值会被立即写回到主内存,这样线程B就可以读取到该变量修改后的值,即共享变量的可见性。
public class Test {
private static volatile int x=1;
public static void modify(){
x=0;
}
public static void main(String[] args) {
new Thread(()->{
while (x==1){
}
System.out.println("x不为1了");
},"A").start();
try {
Thread.sleep(3000);
modify();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main over");
}
}
如上,睡眠3s后调用modify修改x的值,如果不使用volatile,x的修改对线程A不可见,程序永远不会结束;而如果用volatile,变量x的修改对A可见,修改后会立即执行完毕A线程。
重排序
重排序是从其他线程的角度看的。
分为编译期重排和运行时重排。
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
如对于:
int a=1;
int b=2;
int c=a*b;
jvm执行时不一定是按照a=1然后b=1的顺序,也可能是先给b赋值,再给a赋值,但不会影响最终c的结果,而这在多线程环境下出现一些问题。
比如我们一个线程在修改一些用户信息,修改完毕后给另一个线程发一个信号,告诉它去用最新的用户信息做统计,如果不对信号标志使用volatile,则可能在修改完信息前,该标志被置为true,也就是说信号标志被重排到了修改中或修改前,这样另一个线程收到信号后,未必得到真正修改完毕的用户信息。
内存屏障
volatile基于内存屏障解决可见性和重排序。https://www.jianshu.com/p/ef8de88b1343
内存屏障是屏障指令,使CPU对屏障指令之前和之后的内存操作执行结果的一种约束,其实就是强制性的将最新的值刷新到主存
happens-before
先行发生原则,volatile的内存可见性就体现了该原则。
线程A:
int k=1
线程B:
int j=k;
线程C:
k=2;
八大原则:
-
程序次序规则
在同一个线程中,书写在前面的操作happen-before后面的操作。
-
锁规则
同一个锁的unlock发生在此锁的lock操作前
-
volatile变量规则
-
线程启动规则
同一个线程的start()在该线程的其他操作前执行
-
线程中断规则
对线程的intrerrupt的调用在被中断线程检测到中断发送前
-
线程终止规则
线程所有其他操作在线程终止前结束
-
对象创建规则
一个对象的初始化先于其finalize方法
-
传递性
a在b前happen-before,b在c前happen-before,那么a 就happen-before C。
场景:
-
不能修饰写入操作依赖当前值的变量,如:num++、num=num+1,JVM字节码层面不止一部,要求常经理本身是原子的
-
由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱