原子性,内存可见性和重排列-synchronized和volatile

来源:互联网 发布:华为云计算哪个部门好 编辑:程序博客网 时间:2024/06/09 22:02

原子性

原子性操作指相应的操作是单一不可分割的操作,例如。对int的类型的变量count的操作count++的操作就不是原子性操作,因为count++ 的操作实际上可以分成三哥操作

读取变量count当前的值

用count的当前值和1做加法运算

将count和当前值增加1后的值赋值给count变量

 

在多线程环境中,非原子性操作可能会受其他线程的干扰。比如上述例子如果没有对相应的代码进行同步(Synchronization)处理,则可能在执行第二个操作的时候,count的值已经被其他线程先修改了,因此这一步操作的count变量的“当前值”已经是过期了的。当然,synchronized可以帮助我们实现操作的原子性,以避免这种线程间干扰的情况

 

synchronized关键字可以实现操作的原子性,其本质是通过该关键字所包括的临界区(Critical Section)的排他性保证任何一个时刻只有一个线程执行临界区的代码。这使得临界区的代码代表了一个原子操作。这点我们可能已经很清楚,但是sychronized 关键字所起的另一个作用—保存内存的可见性(Memory Visibility),也是需要我们注意的。

 

CPU在执行代码的时候,为了减少变量访问的时间消耗,可能将代码中访问的变量的值缓存在该CPU的缓存区(如L1 Cache,L2 Cache等)中。因此相应代码再次访问某个变量的时候可能就是从CPU的缓存区中取而不是到内存中取。同样的,代码对这些缓存过的变量的修改也只可能写到了CPU的缓存区中,没有写会到主存里。由于每个CPU都有自己的缓存区,所以一个CPU缓存中的内容对于其他CPU而言是不可见的。这就是导致了其他CPU上运行的其他线程可能无法“看到”该线程对某个变量值所做的改变,这就是内存可见性

 

synchronized关键字的另一个作用就它保证了一个线程执行临界区中的代码时所修改的变量值对于稍后执行该临界区中的代码的线程来说是可见的。这对于保证多线程的代码的正确性来说非常重要。

 

而volatile关键字也能够保证内存的可见性,即一个线程对一个采用volatile关键字所修饰的变量的值的更改对于其他访问该变量的线程而言是可见的。也就是说,其他线程不会读到一个“过期”的变量值。因此,有人将volatile关键字与synchronized关键字所代表的内部锁比较,称其为轻量级的锁,这种称呼其实并不恰当,因为volatile关键字只能保证内存可见性,它并不能像synchronized关键字所代表的内部锁那样能够保证操作的原子性。volatile关键字的实现内存可见性的核心机制是当一个线程修改了一个volatile修饰的变量的值时,该值会被写入主内存(RAM)中,而不仅仅是当前线程所在的CPU的缓存区,而其他CPU的缓存区存储的该变量的值也会因此而失效(从而得以更新为主内UC你中该变量的相应值)。这就保证了其他线程访问该volatile修饰的变量总是可以获取该变量的最新值。

 

volatile关键字的另一个作用就是他禁止了指令重新排序(Re-order,编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行的。例如下面的实例初始化语句为

 

private SomeClass obj = new SomeClass();


上述语句所做的事情非常简单

创建类SomeClass的一个实例

将类SomeClass的实例的引用赋值给obj。

但是由于指令重排序的作用,这段代码的执行的顺序可能是

分配一个用于存储SomeClass实例的内存空间

将对该内存空间的引用赋值给变量obj

创建一个someClass的实例

因此,当其他线程访问obj变量的值的时候,其得到的可能仅仅是一个指向一段存储SomeClaas实例的内存空间的引用而已,而该内存空间相应的SomeClass的实例实例可能尚未初始化完成。这就可能导致一些意向不大的结果。而禁止指令重新排序可以使得上述代码按照我们所期望的顺序(正如代码所表达的顺序)来执行。

 

禁止指令重排序虽然导致编译器和CPU无法对一些指令进行优化,但它在某种程度上让代码的执行看起来更符合我们的期望。

 

与sychronized相比,前者既能保证操作的原子性,又能保存内存的可见性。而volatile仅能保存内存的可见性。

 

1 0
原创粉丝点击