一、并发编程Bug源头
1. 缓存一致性问题
缓存导致的可见性问题:具体来说就是缓存不一致问题,例如下面的例子:
单核时代下,所有线程用一个缓存,因此不会产生缓存一致性问题。但是在多核时代下
同一个内存中的变量v在缓存中可能有不同的副本,缓存不一致问题存在,但是有许多协议能够保证缓存的一致性。 有MESI等协议。如下面的代码:
public class Test {
private long count = 0;
private void add10K() {
int idx = 0;
while(idx++ < 10000) {
count += 1;
}
}
public static long calc() {
final Test test = new Test();
// 创建两个线程,执行 add() 操作
Thread th1 = new Thread(()->{
test.add10K();
});
Thread th2 = new Thread(()->{
test.add10K();
});
// 启动两个线程
th1.start();
th2.start();
// 等待两个线程执行结束
th1.join();
th2.join();
return count;
}
}
两个线程分别循环给count加一,但是由于两个线程可能会交替运行,去读取内存中可能已经失效的值,导致最终结果20000以下。
2.原子性问题
cpu对于每个程序都有着时间片,时间片一到就会被中断,因此不具有原子性。
一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。
3.编译优化带来的有序性问题
双重检查创建单例对象:
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
这个双重检查单例对象创建看起来貌似没有什么问题,但是值得注意的是,假如有两个线程线程A成功的检查到了instance==null,此时新创建一个对象赋值给instance,但是如果new的顺序是,分配内存,创建&M变量,将地址赋值给instance则不会产生异常。但是new的实际执行为:
1.分配内存 2.地址赋值 3.初始化对象
如果在2之后线程被中断则会导致此时虽然instance不是一个空的指针,但是Instance指向了一个没有初始化的对象。
二、JAVA内存模型:解决可见性有序性问题
最早诞生于C语言原始的意义就是禁用cpu缓存。
java 1.5 Happens-Before规则:前一个操作的结果对于后续操作具有可见性
① 程序顺序性规则:
同一线程中操作顺序不变(例如,线程的操作是a,b,c,d,则最终的顺序也必须是a,b,c,d)
② volatile变量规则:
写操作必须可见与后面的读操作
③ 传递性
已知 A线程中x = 42 应当先于v = true 变量赋值执行,读v变量在写v变量之后执行,因此最终顺序如上图所示。
④ 管程中的锁
解锁必须在加锁之后,这一点在Java中是自动实现的。
synchronized (this) { // 此处自动加锁
// x 是共享变量, 初始值 =10
if (this.x < 12) {
this.x = 12;
}
} // 此处自动解锁
⑤ 线程start( )规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
⑥ 线程join() 规则
指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。
其实就是同步,整个并行处理的过程无非就是同步并行同步并行等等。
被忽视的final关键字:
三、互斥锁
简易锁模型:
Java 语言提供的锁技术:synchronized
默认:当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就是 Class X;
当修饰非静态方法的时候,锁定的是当前实例对象 this。
class SafeCalc {
long value = 0L;
synchronized long get() {
return value;
}
synchronized void addOne() {
value += 1;
}
}
这里保证了同一时刻只有一个线程才可以调用addone()方法,synchronized关键字给this加锁,其他线程如果要调用这个方法首先需要获得this对象。
由于get方法也在一个保护区里面所以和addone()是互斥的。
class SafeCalc {
long value = 0L;
long get() {
synchronized (new Object()) {
return value;
}
}
void addOne() {
synchronized (new Object()) {
value += 1;
}
}
这里每次加锁的不是一个对象,因此不能做到互斥。