原子(Atomic)操作指相应的操作是单一的不可分割的操作。例如,对int型变量count进行count++的操作就不是原子操作。这是因为count++实际上可以分解为3个操作:
1)读取变量count的当前值;
2)拿count的当前值和1做加法运算;
3)将count的当前值增加1后的值赋值给count变量。
1.Synchronized
在多线程环境中,非原子操作可能会受到其他线程的干扰。比如,上述例子中如果没有对相应的代码进行同步(Synchronization)处理,则可能会出现在执行第2个操作的时候count的值已经被其他线程更改了,因此,这将会导致这一步的操作所使用的count的是已经是过期的了。在这里需要说明synchronized关键字可以帮助我们实现操作的原子性,以避免这种线程间的干扰情况。
package com.stk.AboutSynchronized;
/***
* 非线程安全的计数器
* @author 斯塔克
*
*/
public class NotSafeThreadCounter {
private int count = 0;
public void addCount() {
count++;
}
public int getCount() {
return this.count;
}
}
package com.stk.AboutSynchronized;
/***
* 线程安全的计数器
* @author 斯塔克
*
*/
public class SafeThreadCounter {
private int count = 0;
public void addCounter() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
synchronized关键字可以实现操作的原子性,其本质是通过该关键字所包括的临界区(Critical Section)的排他性从而保证在任何 一个时刻只有一个线程能够执行临界区中的代码,这样就使得临界区中的代码代表了一个原子操作;除此之外,synchronized关键字所起的另外一个作用---保证内存的可见性(Memory Visibility),也是非常重要的。
CPU在执行代码的时候,为了减少变量访问的时间消耗可能将代码中访问的变量的值缓存到该CPU的缓冲区中。因此当相应的代码再次访问某个变量时,相应的值可能是从CPU缓存区而不是从主内存中读取的。同样的,代码对这些被缓存过的变量的值的修改也可能仅是被写进CPU的缓存区,而没有被写回到主内存。由于每一个CPU都有自己的缓存区,因此一个CPU缓存区中的内容对于其他CPU而言是不可见的。这就导致了在其他CPU上运行的其他线程可能无法“看到”该线程对某个变量值所做的更改。这就是所谓的内存的可见性。
synchronized关键字的另外一个作用就是它保证了一个线程执行临界区中的代码时所修改的变量值对于稍后执行该临界区中的代码的其他线程来说是可见的。因此这对于保证多线程代码的正确性来说是非常重要的。
2.Volatile
volatile关键字也能够保证内存可见性。即,一个线程对一个采用volatile关键字所修饰的变量的值的更改对于其他访问该变量的线程而言总是可见的。换句话说,其他线程不会读到一个“过期”的变量值。但是volatile关键字和synchronized关键字所不同的是volatile关键字只能保证内存的可见性,它并不能像synchronized关键字所代表的内部锁那样能够保证操作的原子性。volatile关键字实现内存可见性的核心机制是当一个线程修改了一个volatile修饰的变量的关键值时,该值会被写入主内存(即RAM),而不仅仅是当前线程所在的CPU的缓存区,而其他CPU的缓存区中存储的该变量的值也会因此而失效,从而得以更新为主内存中该变量的相应值。这就保证了其他线程访问该volatile修饰的变量时总是可以获取该变量的最新值。
volatile关键字的另外一个作用是他禁止了指令重排序(Reorder)。编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行的。例如下面的梳理变量初始化语句:
private SomeClass someObject = new SomeClass();
上述语句所做的事情包括:
1)创建类SomeClass的实例;
2)将类SomeClass实例空间的首地址值赋值给对象someObject;
由于指令重排序的作用,这段代码的实际执行顺序可能是:
1)分配一段用于存储SomeClass实例的内存空间;
2)将该内存空间的首地址值赋值给对象someObject;
3)创建类SomeClass的实例。
因此,当其他线程访问someObject对象的值时,其得到的仅是指向一段存储SomeClass实例的内存空间的引用而已,而该内存空间相应的SomeClass类的实例的初始化可能还尚未完成,这就可能导致一些意想不到的结果。而禁止指令的重排序可以使得上述代码按照我们所期望的顺序(正如代码所表达的顺序)来执行。禁止指令的重排序虽然导致编译器和CPU无法对一些指令进行优化,但是它某种程度上让代码的执行看起来更符合去我们的期望。
3.总结
synchronized与volatile相比较而言synchronized既能保证操作的原子性,又能保证内存可见性;而volatile仅能保证内存可见性。但是,前者会导致上下文切换,而后者不会。