文章目录
一、CAS原理
CAS全称CompareAndSet或者CompareAndSwap,其中【比较-交换】的操作是原子的,下面先一个具体的案例
1.1 无锁保护共享变量
定义一个接口
interface Acount {
void withdraw(int amount);
Integer getBalance();
//设置余额为10000,一千个线程每个线程减少10,正确结果应该为0
static void demo(Acount acount) {
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
threadList.add(new Thread(() -> acount.withdraw(10)));
}
long start = System.currentTimeMillis();
threadList.forEach(Thread::start);
threadList.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.printf("执行时间:%d,余额:%d", end - start, acount.getBalance());
}
}
1.1.1 不安全模式实现
不安全的实现
class AcountUnsafe implements Acount {
private Integer balance;
public AcountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public void withdraw(int amount) {
this.balance -= amount;
}
public Integer getBalance() {
return balance;
}
public void setBalance(Integer balance) {
this.balance = balance;
}
}
测试
public class Test1 {
public static void main(String[] args) {
Acount acount = new AcountUnsafe(10000);
Acount.demo(acount);
}
}
结果:
1.1.2 有锁安全实现
给withdraw加锁即可
@Override
public void withdraw(int amount) {
synchronized(this){
this.balance -= amount;
}
}
结果:
1.1.3 无锁安全实现
无锁采用原子整数AtomicInteger来实现
class AcountSafe implements Acount {
private AtomicInteger balance;
public AcountSafe(AtomicInteger balance) {
this.balance = balance;
}
@Override
public void withdraw(int amount) {
while (true) {
int prev = this.balance.get();
int value = prev - amount;
// compareAndSet是JVM提供的本地方法,该方法具有原子性
if (this.balance.compareAndSet(prev, value)) {
break;
}
}
}
public Integer getBalance() {
return balance.get();
}
}
结果
比较执行时间发现,无锁比有锁实现要快一点
1.2 CAS工作方式
先分析下上述代码的时序图
CAS底层使用的lock compxchg指令(X86架构),在单核CPU和多核CPU下都能保证【比较-交换】的原子性。CAS必须借助volatile才能读到共享变量的最新值,从而完成【比较-交换】的效果,来看看AtomicInteger的源码,其中初始值value就是用volatile修饰的
注意,volatile只能保证可见性,让其他线程看到最新值,但不能保证原子性即不能解决多个线程的指令交错问题。
1.3 CAS的效率和特点
CAS的作用是用无锁来实现多线程对共享变量操作的安全性,由于需要不停重试它也不是一定能提高效率。
CAS的效率:
- 无锁的情况下,即使重试失败,线程仍然在高速运行。synchronized加锁会让线程进入阻塞,发生上下文切换。
- 线程上下文切换,好比赛车在跑动高速运行时,需要先刹车减速停下之后再被唤醒之后重新启动、加速,代价比较大。
- 无锁情况下,线程要保持运行,需要CPU支援。在单核CPU下,线程不加锁也会由于时间片使用完,发生上下文切换。因此,CAS需要在多核下才能发挥优势,而且线程数最好不要超过CPU核数。
CAS的特点:
- CAS基于乐观锁的思维实现,不怕别人修改共享变量,修改了没关系,自己再重试
- synchronized基于悲观锁的思维,加锁之后不允许别人修改共享变量,除非自己修改完释放锁,别人才有机会
- CAS体现的是无锁并发、无阻塞并发。由于没有加锁,不会发生阻塞,从而提高效率。但是在竞争激烈的情况下,会发生大量的无效重试,反而会影响效率。
二、原子整数
2.1 AtomicInteger、AtomicBoolean、AtomicLong
AtomicInteger、AtomicBoolean、AtomicLong,这三个比较类似,都可以看做是对一个整数的封装。
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
//自增等价于++i
System.out.println(i.incrementAndGet());
//i++
System.out.println(i.getAndIncrement());
System.out.println(i.get());
//类似的还有--i,i--
//增加指定的数
System.out.println(i.addAndGet(3));
System.out.println(i.getAndAdd(3));
System.out.println(i.get());
//其他计算,乘法等复杂运算
System.out.println(i.updateAndGet(x -> x * 2 - 1));
System.out.println(i.getAndUpdate(x -> x * 2 - 1));
System.out.println(i.get());
//自己实现updateAndGet
System.out.println(updateAndGet(i,x->(x-1)*2));
}
private static AtomicInteger updateAndGet(AtomicInteger i, IntUnaryOperator intUnaryOperator) {
int prev;
do {
prev = i.get();
} while (!i.compareAndSet(prev, intUnaryOperator.applyAsInt(prev)));
return i;
}
}
2.2 Unsafe模拟AtomicInteger
public class MyAtomicIntegerTest implements Acount {
public static void main(String[] args) {
Acount test = new MyAtomicIntegerTest(10000);
Acount.demo(test);
}
MyAtomicInteger balance;
public MyAtomicIntegerTest(int i) {
this.balance = new MyAtomicInteger(i);
}
@Override
public void withdraw(int amount) {
balance.incrementAndGet(-amount);
}
@Override
public Integer getBalance() {
return balance.get();
}
}
class MyAtomicInteger {
volatile int value;
private static Unsafe UNSAFE;
private static long valueOffset;
static {
try {
//利用反射获取Unsafe的实例
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
UNSAFE = (Unsafe) field.get(null);
//获取字段value相对类对象内存开始位置的偏移量
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public MyAtomicInteger(int value) {
this.value = value;
}
public int get() {
return value;
}
public void incrementAndGet(int i) {
while (true) {
if (UNSAFE.compareAndSwapInt(this, valueOffset, this.get(), this.get() + i)) {
break;
}
}
}
}
测试结果:
三、原子引用
3.1 AtomicReference
使用AtomicReference实现Account
public class AtomicReferenceTest {
public static void main(String[] args) {
Acount acount = new AccountBigDecimalSafe(new BigDecimal("10000"));
Acount.demo(acount);
}
}
class AccountBigDecimalSafe implements Acount {
private AtomicReference<BigDecimal> balance;
AccountBigDecimalSafe(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
@Override
public void withdraw(int amount) {
while (true) {
BigDecimal prev = balance.get();
BigDecimal next = prev.subtract(new BigDecimal(amount));
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
@Override
public Integer getBalance() {
return balance.get().intValue();
}
}
3.2 ABA问题
字符串A,先改成了B又改回A,主线程发现字符串还是A,就改成了C。一般ABA问题不会有什么影响,但在实际工作中还是注意。
public class AbaTest {
public static void main(String[] args) {
AtomicReference atomicReference = new AtomicReference("A");
System.out.println(atomicReference);
new Thread(() -> {
if (atomicReference.compareAndSet("A", "B")) {
System.out.println(atomicReference);
}
if (atomicReference.compareAndSet("B", "A")) {
System.out.println(atomicReference);
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicReference.compareAndSet("A", "C")) {
System.out.println(atomicReference);
}
}
}
3.3 AtomicMarkableReference
可以使用AtomicMarkableReference解决ABA问题
public class AbaTest2 {
public static void main(String[] args) {
AtomicMarkableReference<String> atomicReference = new AtomicMarkableReference("A",false);
System.out.println(atomicReference.getReference());
new Thread(() -> {
if (atomicReference.compareAndSet("A", "B",false,true)) {
System.out.println(atomicReference.getReference());
}
if (atomicReference.compareAndSet("B", "A",false,true)) {
System.out.println(atomicReference.getReference());
}
}).start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicReference.compareAndSet("A", "C",false,true)) {
System.out.println(atomicReference.getReference());
}
}
}
AtomicMarkableReference增加了是否改动过的标记,只要改动过,并被标记上,后面的改动就无法生效。
四、原子累加器
原子累加器,指的是累加操作是原子的,java8之前只能使用原子整数类的incrementAndGet方法,java8新增了LongAdder(并发大神狗哥,Doug Lea编写),专门用于累加操作,效率提升5倍。
public class LongAddrTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
addr(() -> new AtomicLong(0), (AtomicLong::incrementAndGet));
}
for (int i = 0; i < 5; i++) {
addr(LongAdder::new, LongAdder::increment);
}
}
static <T> void addr(Supplier<T> supplier, Consumer<T> action) {
T t = supplier.get();
List<Thread> threads = new ArrayList<>(4);
long start = System.currentTimeMillis();
for (int i = 0; i < 4; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(t);
}
}));
}
threads.forEach(Thread::start);
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start));
}
}
测试结果:
LongAddr性能提升的原因是拆分了多个累加单元[cell0]…,当竞争激烈的时候,累加分散到多个cell减少失败重试,最后将结果汇总,最终减少竞争提高效率。