线程安全之原子操作
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.处理器如何实现原子操作
一般是通过锁总线,或者锁CPU缓存。
锁总线:如果多个CPU同时读改写共享变量(例如i++),其中某个CPU通过一个LOCK信号,把总线资源据为己有,就能实现原子操作了,此时其他CPU无法操作总线,拥有总线资源的CPU独占共享内存。
锁CPU缓存:锁总线开销太大了,等于多核CPU变成了单核CPU。一般来说,在同一时刻,我们只需要保证某个内存地址的操作是原子性的就可以了,锁总线让其他CPU在锁定期间,不能访问内存数据,这绝不允许。因此使用缓存锁,对总线锁进行优化。缓存锁是通过缓存一致性机制,保证对共享变量修改的原子性。因为实现了缓存一致性协议的多核处理器,不允许多个CPU同时修改它们缓存的相同内存区域。举个例子,CPU-A缓存了共享变量x,CPU-B也缓存了共享变量x,当CPU-A在回写x到主内存的时候,这期间CPU-B不能回写x到主内存,并且当CPU-A回写完毕,CPU-B缓存的共享变量x此时是无效的,如果接下来CPU-B仍然要操作共享变量x,会强制从主内存重新读数据到缓存。
P.S.如果内存数据不能被缓存,或者操作的数据跨多个缓存行,此时就必须锁总线;有的处理器不支持锁缓存,那只好锁总线。
4.Java如何实现原子操作
1.使用循环CAS操作 2.使用锁机制,锁保证了只有一个线程能进入同步代码块,操作共享内存数据,有意思的是,大部分锁也都用到了CAS操作,即进入同步代码块的时候,循环CAS获取锁,退出同步块的时候,循环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对象。
5.JDK提供的原子操作类
第三节提到的Unsafe,一般不用。如果想做CAS操作,JDK为我们提供了java.util.concurrent.atomic包下面的原子操作类。下面来模拟一个场景,在2秒时间里,开启3个线程对同一个变量执行自增操作,观察用synchronized锁能自增多少次,用AtomicLong能自增多少次,用LongAdder能自增多少次。以此来对比这三者的性能。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;
public class TestCAS {
public static Object lock = new Object();
public long count = 0;
public void testSync(){
for(int i = 0 ; i < 3; i++){
new Thread(() -> {
long startTime = System.currentTimeMillis();
while(System.currentTimeMillis() - startTime < 2000) {
synchronized (lock){
count++;
}
}
long spendTime = System.currentTimeMillis() - startTime;
System.out.println("synchronized锁自增花销" + spendTime + "毫秒,自增结果" + count);
}).start();
}
}
public AtomicLong atomicLong = new AtomicLong(0);
public void testAtomicLong(){
for(int i = 0 ; i < 3; i++) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 2000) {
atomicLong.incrementAndGet();
}
long spendTime = System.currentTimeMillis() - startTime;
System.out.println("AtomicLong自增花销" + spendTime + "毫秒,自增结果" + atomicLong.get());
}).start();
}
}
public LongAdder longAdder = new LongAdder();
public void testLongAdder(){
for(int i = 0 ; i < 3; i++) {
new Thread(() -> {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < 2000) {
longAdder.increment();
}
long spendTime = System.currentTimeMillis() - startTime;
System.out.println("LongAdder自增花销" + spendTime + "毫秒,自增结果" + longAdder.sum());
}).start();
}
}
public static void main(String[] args){
new TestCAS().testSync();
new TestCAS().testAtomicLong();
new TestCAS().testLongAdder();
}
}
结果是LongAdder > AtomicLong > synchronized,这是因为在高并发情况下,LongAdder将对单一变量的CAS操作,分散为对多个变量的CAS操作,让不同线程的CAS操作对象不同,降低了CAS失败的概率,在取值时,再进行求和,有点分布式计算的感觉。AtomicLong就是正常的对单一变量的CAS操作,全部线程操作同一个变量,如果修改失败就自旋,这种方式的劣势在于,如果操作比较耗时,且一直失败,就会一直占用CPU资源,而越是高并发,就越可能冲突,越发生冲突,就越占用CPU,这是恶性循环,而LongAdder采用分段的策略,在低并发的情况下,各个线程就执行对单一变量的CAS操作,这种情况和AtomicLong性能差不多,到了高并发的环境下,LongAdder会增加变量的分身,让一些线程对分身做CAS操作,通过增加分身的数量,减少CAS冲突的概率,到了计算总数时,再将所有分身进行求和运算,Doug Lea不愧是设计大师。synchronized关键字就等于一刀切了,管你能不能执行成功,都给我按失败的可能来,那就都乖乖排队,顺序自增。
6.ABA问题
CAS操作的核心,是检查值有没有变化,那如果一直值原来是A,中间变成了B,再变成了A,那么用CAS检查会认为它没有变化,实际上是变化了。这就是ABA问题。解决这个问题的思路是用版本号,在变量值的前面加版本号,每次变量更新的同时,版本号加1,这样就是1A-2B-3A,这样就能检查到值的变化,避免ABA问题。