线程安全之原子操作
1.首先引入2个关键字:竞态条件、临界区
public class TestCase {
public int i = 0;
public void incr(){//incr方法内部,就是临界区
i++;//实际开发中,这里可能是某个计数器,某个积分计算等业务代码,而不是简单的i++
}
}
一般来说,多线程在同一时刻访问某一共享资源,在对共享资源做写操作时,需要对执行顺序有所要求。例如上述代码中的incr方法内部,就是临界区,多线程并发执行i++,会对执行结果产生影响。竞态条件,是在临界区内的特殊条件。换句话说,单线程环境下,不存在竞态条件。多线程执行incr方法中的i++代码,就可能产生竞态条件。
归纳一下:线程不安全的代码,在临界区里。临界区里的代码,因为产生了竞态条件,所以线程不安全。如果破坏了竞态条件,那么线程安全。如果没有竞态条件,也根本写不出线程不安全的代码。再举个例子,如果一个对象是栈封闭的,也就是其引用是在线程的栈里,当线程运行结束后,引用也就没了,这样的对象也是线程安全的。还有一种对象也是线程安全的,那就是不可变对象,例如某个对象是final类型的,那不管什么线程,都不能够对该变量进行写操作,只能读,这样的对象也是线程安全的。
2.volatile不能保证原子性
我们知道,volatile能够保证可见性,让变量的更改能立即被其它线程看见,但是,假设有3个线程同时进入临界区:
public void incr(){
//3个线程,同时进入这里,各自读取i都为0,此时A线程先进行加1操作,然后线程切换,B线程也加1,再切换,C线程也加1。
//接下来,A线程进行写操作,令i=1。此时i的值对B、C线程可见(B、C看到i=1),但是B线程接下来并不会再加1了,而是进行写操作,令i=1,同理C线程也是写操作,i=1。
//最终结果,i=1。程序创建3个线程,各执行1次自增操作,得到的结果却只自增了一次。这是因为volatile不能保证原子性,对一个变量修饰volatile,不代表对该变量的操作是原子的。
i++;
}
3.CAS操作
CAS是硬件提供的同步原语,Compare And Swap,比较且交换,由处理器保证内存操作的原子性。CAS操作需要输入2个数值,一个旧值A,一个新值B。在赋值前先比较旧值是否和A相等,如果相等,就赋新的值B,如果不等就操作失败。这其中的比较和交换2个操作,是原子的。下面来一个CAS操作的示例。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestCAS {
static Unsafe unsafe;
static {
try {
//前3行通过反射拿到Unsafe对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//获取value属性的偏移量,也就是value属性的内存地址
valueOffset = unsafe.objectFieldOffset(TestCAS.class.getDeclaredField("value"));
}catch (Exception e) {
e.printStackTrace();
}
}
public int value = 0;//CAS自增
public int value2 = 0;//普通自增
public static long valueOffset;//value在内存中的偏移量
//每一次自增操作,都是一次循环CAS直到CAS成功。
public void incr(){
int temp;
do{
//通过value的偏移量拿到value的值,初始值是0
temp = unsafe.getInt(this, valueOffset);
}while (!unsafe.compareAndSwapInt(this, valueOffset, temp, temp + 1));//如果CAS失败,就循环CAS
}
public void incr2(){
value2++;
}
public static void main(String[] args){
TestCAS testCAS = new TestCAS();
Thread th1 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10000;i++){
testCAS.incr();
testCAS.incr2();
}
}
});
Thread th2 = new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0;i<10000;i++){
testCAS.incr();
testCAS.incr2();
}
}
});
th1.start();
th2.start();
try {
th1.join();
th2.join();
}catch (Exception e){
}
System.out.println(testCAS.value);//20000
System.out.println(testCAS.value2);//小于20000
}
}
上述代码就是一个CAS底层操作示例。已经算很底层了,用的是Unsafe对象。这段代码还是我用idea开发工具写的,Eclipse我甚至不知道怎么拿到Unsafe对象。
4.JDK提供的原子操作类
第三节提到的Unsafe,一般不用。如果想做CAS操作,JDK为我们提供了java.util.concurrent.atomic包下面的原子操作类。下面来模拟一个场景,在2秒时间里,用synchronized锁能自增多少次,用AtomicLong能自增多少次,用LongAdder能自增多少次。以此来对比这三者的性能。