java的线程内存模型分为了主内存和工作线程, 由于线程运行于不同的CPU中, 所以说不同的线程是没办法读取到彼此之间的内存数据的, 而多个线程之所以看起来能够操作主内存的某一个共享变量, 实际的过程是各个线程把此变量copy到自己的工作内存中,实际是对copy的操作,最后刷入到主内存中。而此过程中各个线程无法确保自己copy到的变量的值就是最新的值, 比方说十个线程对同一个变量执行+1操作, 可能第十个线程去copy这个变量的时候其他九个线程还没把最新的执行结果刷入到主内存中,导致第十个线程+1得到的结果不等于原来的变量+10的期望值。所以需要来保证线程的安全性
synchronized:
synchronized修饰的是某一个变量或者方法, 保证它所在的内存块加了一个同步锁, 每一个线程对synchronized修饰过了的内存区域操作的时候都会获取同步锁, 只有获取了同步锁的线程才可以操作住内存, 在释放同步锁的时候把最新值刷入到住内存中。依靠锁的唯一可获取性保证了共享内存的正确性.
volatile:
保证了多个线程之间共享变量的可见性, (默认的情况下操作一个共享变量是先把这个变量copy到自己的工作内存执行操作的, 导致其他线程对共享内存的最新值的不可见性)
既然synchronized的原理是给内存块添加同步锁,那么volatile的实现原理是什么呢?
通过编译后对比没有添加volatile的变量, 发现有volatile变量编译后多了一条指令:
lock addl $0x0,(%esp);
这条指令保证了两件事:
- 当前线程的工作区域中被volatile修饰的内存块的数据刷新到主内存中;(保证主内存中是最新值)
- 刷新当前线程的工作内存中变量值到主内存的同时还会导致其他线程中的工作内存中缓存的该共享变量地址指向的数据值无效.(剔除了非最新值对住内存中共享变量值污染的可能性)。
虽然volatile的消耗低,但是volatile只是提供了变量的可见性(具体方式参考上面说的),却没有提供原子性,(synchronized的原子性由锁的特征提供).这会导致一些问题,诸如多线程计数不准确问题.
原子操作指的是一组不可被中断的操作, 以i++为例子, 它实际上包含了load(from main memory) , add(in work memory) ,save(to main memory) 三个操作, 在多处理器环境中同时执行这个操作就可能导致这组指令被打断, 可能在cpu1在执行add指令还没执行到save指令的时候, cpu2就从main memory中load i的值此时的i并非是最新的value. 因此说i++操作不是一个原子性的操作. 需要借助concurrent包里面的atomaticInteger来实现此目的, 它确保了一个操作的原子性, 那么是如何实现的呢?
volatile实现了一个目标: 确保了其他线程读取到的i变量是最新的value,然而离计算出正确的i的值还缺少一个步骤: 如何保证多个线程在load到最新的值之后依次的把各自内存中修改过后的变量i save到主内存中? 然而volatile限制了这个功能: 被volatile修饰过的变量的值一旦被改动,将会导致拷贝了此变量到工作内存中的其他线程无法再次操作这个变量的副本, 也无法save回主内存. 所以用volatile的话还是得同时配合synchronized来实现线程安全的目标啊.例如DCL
那么原子操作的实现原理是什么呢?
锁就不说了,说说compare and swap算法来实现自旋CAS —> 自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止
那么单次的CAS是怎样的呢?
boolean compareAndSwap(int mainmemoryValue,int workmemoryOldValue,int workMemoryNewValue){
if(mainMemoryValue != workMemoryOldValue){
return false;
}
mainmemoryValue = workMemoryNewValue;
return true;
}
以上是java伪代码, 实际上的实现是c++完成的, 唯一的不同就是此算法的第一个参数是一个指向mainMemory的变量的一个指针, 只有当来自workMemory的变量值等于主内存中该变量的值的时候才能认为是衔接上一条指令的操作,将workMemory中最新的值刷入到主内存指针指向的value.从而保证了一组操作的原子性。
CAS依旧存在的限制:
ABA问题:
两个进程, 一个进程在load到主线程变量A的值后因某种原因挂掉了, 后来的进程2再次load这个变量A把他的值从1改到了10再从10改回了1, 此时的进程1重启了发现变量A的值没有改变, 那么进程1是无法感知到进程2对变量a的1->10,10->1的这个过程。可能在java中只是值的变动感知不到这个影响,但是如果发生在c++中的指针比较交换的话,就容易发生问题: 具体的例子参考wiki
由于自旋的过程就是不断的执行cas,倘若cas长时间不成功, 就导致cpu长时间的空转