【今天重温了大神写的并发相关文章】
概念定义
可见性:一个线程对共享变量的修改,另外一个线程能够立刻看见,我们称之为可见性。
任务切换、时间片:操作系统允许某个线程执行一小段时间,例如50ms,过了50ms操作系统就会重新选择一个进程来执行,这个过程叫做“任务切换”,其中50ms就叫做时间片。
原子性:一个或者多个操作在CPU执行的过程中不被中断的特性称为原子性。
为什么并发程序容易出问题呢?
1 缓存导致的可见性问题
在单核系统中,所有的线程操作的都是同一个CPU的缓存,一个线程对缓存的读写,另一个线程一定是可见的。假如有共享变量A,如果线程一改变了它的值,那么线程二再访问A的时候,得到的一定是被线程一修改过的A的最新值,这个过程就是可见性。
在多核系统中,多个线程在不同的CPU上运行,这些线程操作的是各自所在CPU的缓存,借用上面的场景来说明,如果CPU1的线程改变了共享变量A的值,那么CPU2的线程是感知不到的,所以不同CPU的线程在操作共享变量时就不具备可见性,这是目前硬件的客观条件造成的,算是个“硬件坑”。
2 线程切换带来的原子性问题
Java并发程序都是基于多线程的,自然会涉及到任务切换。任务切换的时机大多数是在时间片结束的时候,针对高级语言来说,一条命令往往会对应多条CPU指令。
操作系统做任务切换的时机,可以是任何一条cpu指令执行完之后。所以CPU能保证的原子操作是cpu指令级别的,不是高级语言级别,所以高级语言层面的原子性需要专门处理,很多朋友会混淆这里。
3 编译优化带来的有序性问题
有序性导致的问题在Java中有一个经典案例,即单例模式的双重检查锁实现方式。
我们先来看段代码:
双重检查锁看上去很完美,但实际上getInstance方法并不完美,问题出在new操作上,我们以为的new操作应该是如下顺序:
1 分配一块内存M;
2 在内存M上初始化Singleton对象;
3 将M的地址赋值给instance变量。
但是实际上优化后的执行路径是这样的:
1 分配一块内存M;
2 将M的地址赋值给instance变量;
3 在内存M上初始化Singleton对象。
优化后会导致这样一个问题,假设线程A先执行getInstance方法,完成指令2之后发生了线程切换现象,切换到了线程B上,线程B也在执行getInstance方法,结果发现instance已经有了,就直接返回instance来使用,但实际上instance在A线程中没有真正的做到初始化,因为没完成第三步,所以线程B在使用instance的时候一定会发生空指针异常。整个过程如下: