目录
1.可见性
现代处理器即使处理那边亮度写这种简单的操作也是复杂到难以置信。
下面这个例子:
private static int done=0;
public static void main(String[] args) {
//toDo main
System.out.println("this is main!");
Runnable runnable1 =()->{
for(int i=0;i<1000;i++){
System.out.println("r1:"+i);
}
done=1;
};
Runnable r2 =()->{
int i=0;
while(done!=1){
i++;
}
System.out.println("r2:"+i);
};
Executor executor =Executors.newCachedThreadPool();
executor.execute(runnable1);
executor.execute(r2);
}
第一个任务输出1000次之后,然后设置done=1,第二个任务等待done为1,然后输出其等待的时候i自加的次数,我们期望输出:
....
r1:993
r1:994
r1:995
r1:996
r1:997
r1:998
r1:999
r2:11820
但事实上:任务2并没有检测到done为1,一直在等待运行,始终没有终止。
这是什么原因?一般来说,这个结果和缓存和指令重排序有关!
缓存和指令重排序
首先,什么是缓存,为什么缓存?
done被创建之后,放在内存中的一个位置,使用的时候,计算机试图在寄存器或者内存缓存中持有他需要的数据,最后将变化写回到内存。这种混粗机制对于提高处理器性能是必须的。有一些用来同步缓存拷贝的操作,但他们都只有在请求的时候才会执行。
同时指令重排序也与这个现象息息相关:
举个例子:
x=不涉及到y变量的一些操作; y=不涉及到x变量的一些操作; z=x+y;
如上,前两步必须发生在第三步之前。但是前两部可以相互交换顺序。处理器可以并行执行前两步。在上述示例中。循环
while(done!=1) {i++}
可以重排为
if(done!=1) while(true){ i++; }
因为循环体没有改变done的值。默认情况下,优化假设不存在并发内存访问。如果有的话,则需要虚拟机知道。这样虚拟机才知道什么时候组织不恰当指令重排。因此我们需要volatile关键字进行修饰变量done。之后编译器会产生必要的指令确保任务中对done的任何改动对其他任务都是可见的。
但是。volatile修饰符足以解决这种特定的问题。但是这并不是通用的解决方案。接下来,我们引入竞争条件的机制。
2.竞争条件
假设多并行任务更新一个共享计数器。
private static int count=0;
public static void main(String[] args) {
//toDo main
System.out.println("this is main!");
Executor executor =Executors.newCachedThreadPool();
for(int i=0;i<100;i++){
int task_id =i;
Runnable runnable1 =()->{
for(int j=0;j<1000;j++){
count++;
}
System.out.println("task_:"+task_id+":"+count);
};
executor.execute(runnable1);
}
}
可以看到输出结果如下:
this is main!
task_:1:2020
task_:3:3020
task_:0:2020
task_:4:4020
task_:2:2020
task_:5:5048
task_:6:6020
task_:7:7020
task_:9:8039
......
task_:76:31516
task_:49:30932
task_:74:30760
task_:72:30710
task_:70:29935
task_:68:29742
task_:99:40927
task_:95:40576
task_:91:39339
task_:97:38974
我们的愿望是最终结果输出100*1000=100 000,但是我们看到,结果是非常凌乱的。这可能是某些县城在不恰当的时候暂停了。而count++更新操作并不是原子性的。他实际上意味着count=count+1,意味着当前线程取得的值很可能是其他线程所更新到内存中的值,换句话说,count变量被提前抢占了。如下实例:
int count=0; r1=count+1;//线程1计算count+1 ...//线程1被抢占 r2=count+1;//线程2计算count+1 count=r2;//线程2将1存入count ...//线程1重新运行 count=r1;//线程1将1存入内存count
我们可以很清楚的看到,此时count不是我们预想中的2,而是1,这个就是因为语句不是原子性所导致的后果。
如果当前线程在不合适的时候暂停了,并被其他任务取得控制,那么他所访问的变量就可能出现不一致的现象,很多事情就会出错。
解决种种问题的方法就是使用锁,将一部分操作成为不可分的原子操作。但不幸的是,锁也不是解决兵法编程的通用操作。通常都难以恰当的使用它,甚至错误的使用回到是性能下降,甚至”死锁“
3.安全并发的策略
- 限制:尽可能不再任务中间共享数据,当任务需要计数或者其他类似操作,让每个任务都有私有变量,等到任务结束,将这些变量合并。
- 不变性:共享不可修改的变量是安全的;
- 锁:通过授权一次只有一个任务访问共享数据。但是单价也比较大,他减少了并行执行的机会。