在了解原子类之前先来了解一下什么是可见性与原子操作。
可见性
在多核处理器中,如果多个线程对一个变量(假设)进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。也就是说多个线程在共享资源的时候操作的都是主存中的资源。
原子操作
原子操作是不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文”切换(线程的切换)之前执行完毕。
看到这里你第一时间可能会想到volatile 关键字,volatile 关键字实际上只能确保可见性,并不能确保原子性,所以使用volatile 关键字的时候你要保证在类中只有一个可变的域,并且这个域的值不能依赖它之前的值,否则它就无法正常工作了,在实际开发中我们要慎用volatile。
还有一点我们需要知道的是:Java 程序语言中的递增操作并不是原子操作,也就是说它存在着线程安全问题,因为在进行运算的过程中,递增操作涉及到读-写操作。下面是递增操作时的步骤:在计算机底层执行递增操作时会产生一个临时变量,将num 的值赋值给这个临时变量,在num 执行完递增操作时又把临时变量值赋给了num,因此num 的值在递增后仍然是1。我们可以把 num++ 的步骤分为三步:“读-改-写”,这样就很好理解了。
public class Test {
public static void main(String[] args) {
int num = 1;
num = num ++;
/**
* 下面是计算机底层在执行递增操作的过程
*
*int tmp = num;
*num = num++;
*num = tmp;
*/
System.out.println(num); // num = 1
}
}
原子类
在Java SE5 之后引入了诸如AtomicInteger、AtomicLong等特殊的原子性变量类,这些类在机器级具有原子性,我们以AtoicInteger 类为例:下面是AtomicInteger 的构造函数,在创建AtomicInteger 对象时,该对象会有一个初始化的值,可以是默认的0,也可以是自定义的值。
AtomicInteger()
创建具有初始值 0 的新 AtomicInteger。
AtomicInteger(int initialValue)
创建具有给定初始值的新 AtomicInteger。
下面是有参的构造器,我们可以看到在有参构造器上面定义了一个volatile 的变量value ,在构造器中将我们自定义的变量赋值给volatile 关键字修饰的value,这样做确保了该对象在执行时保证了内存可见性。
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
在原子类中通过CAS(CompareAndSet) 算法保证原子性(这里仍然是以AtoicInteger 类为例),在这个方法中我们看到调用了unsafe 类中的静态方法compareAndSwapInt()。因为Java 不能直接访问操作系统底层,而是通过本地方法来访问。Unsafe类提供了硬件级别的原子操作,也就是说通过该类调用操作系统底层的方法用来保证在机器级别的原子操作。
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这样以来原子类不仅保证了内存可见性也保证了原子性,所以在使用它们的时候,我们不用担心会出现并发问题。下面我们就通过AtoicInteger 原子类实现一个简单的并发操作:创建10 个线程并发访问任务,在任务中实现自增操作,为了达到效果让程序睡眠100ms。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicTest {
public static void main(String[] args) {
AtomicDemo demo = new AtomicDemo();
for(int i = 0;i<10;i++){
new Thread(demo).start();
}
}
}
class AtomicDemo implements Runnable{
private AtomicInteger num = new AtomicInteger(1);
@Override
public void run() {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "......" + getNum());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public int getNum(){
return num.getAndIncrement();
}
}
输出:
Thread-4……1
Thread-2……3
Thread-0……2
Thread-1……4
Thread-5……5
Thread-6……7
Thread-8……6
Thread-9……8
Thread-3……9
Thread-7……10
从上面的输出结果中我们可以看出这10 个线程在安全有序的情况下访问着临界区,这里我们并没有通过加锁的机制保证线程安全,原子类通过保证内存可见性与原子操作同样也能达到加锁的目的并且原子类要比加锁的效率要高。
PS:我们只有在特殊的情况下才在代码中使用它们,在使用的前提下我们要保证它不会存在问题,通常情况下还是使用锁机制会更安全一些。
参考书籍:
《Java 编程思想》Bruce Eckel 著 陈昊鹏 译