自增操作符(++),非原子性,线程不安全。线程安全的计数采用 synchronized 或 AtomicInteger 类。
AtomicInteger 类的 getAndIncrement() 方法实现原子操作,基于cas算法,非阻塞同步。
CAS算法
悲观锁,
每次请求数据时,都会认为其他线程修改,每次都会上锁,等用完后,解锁让其他线程使用,synchronized 和ReentrantLock 都是悲观锁。
乐观锁,
比较乐观的认为其他线程不会修改数据,不会上锁,在更新时,会判断有无其他线程更新数据,版本号机制或CAS算法实现,当线程拿到数据时,同时拿到version,更新数据且version++,写回时,发现另一个线程已经改新了数据,并提高了version,则会更新失败的,避免两个线程操作数据相互覆盖。
CAS算法
比较与交换,是一种非阻塞同步,读写的内存V,比较的值A,新值B,多个线程并发操作时,只V的值是A,才会改成B,这一套比较并修改是原子操作,其他线程发现已经是B了,不是A,不修改,会自旋。
在FutureTask中经常见到compareAndSwapInt。
算法问题,ABA问题,自旋,cpu开销,只能保证一个共享变量的原子操作。
ABA问题,
比较初始值A检查时,是A,没问题,可能中间被改动过,又改回了A,会认为没改过。AtomicStampedReference解决,控制变量值的版本解决。
乐观锁在读比较多时适用。atomic。synchronized使用写多场景。
i++,线程先取i值,然后+1,最后存入,一个线程未存入前,另一个线程取到原值,两个线程i++写入对值将相同,两次自增,算作一次。
int count=0;
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++) {
count++;
atomicInteger.getAndIncrement();
countVolatile++;
}
}
}.start();
}
10个线程并发操作count变量,主线程中定义,初始值是0,对count操作时,拷贝副本到线程工作内存,加1,存入主内存。如果是volatile修饰:线程的每次操作count都会有这些步骤,使用时都取新的,保证使用其他线程对count的修改的最新值,更新后,都会写入主内存,确保其他线程可见。
如果没有volatile修饰,线程连续多次操作count时,不会每次都写入或读取最新,估计连续很多次做一次读写操作。总之,count的自增操作不是原子性。
当一个线程读,还未写入时,另一个线程进入,获取主线程当旧值,两个线程当操作是在相同值的基础上进行,如果都只加一次就写入,少算一次自增。volatile可保证可见性,不能保证原子性。最后的值将小于10000。
count:9252
AtomicInteget count:10000
countVolatile:9027
非阻塞同步
AtomicInteger 类的 getAndIncrement() 方法实现原理是非阻塞同步。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
内部使用Unsafe类。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
每次 getAndAddInt() 调用,都会有一个 while 循环,取var2位置的值var5,
compareAndSwapInt是native方法,原子性。当调用它时,会比较当前var2位置是否是var5,如果还是var5,说明其他线程未更新,将var5+var4,var4就是1,更新后当值写入var2位置。该操作是原子操作,保证线程安全。
如果这时已经不是var5,说明有线程已经更新值,返回false,继续循环,再去取值,这个过程并没有线程休眠,因此是非阻塞并发。
任重而道远
网友评论