文章目录
- 一、CAS原理
- 1.1 无锁保护共享变量
- 1.1.1 不安全模式实现
- 1.1.2 有锁安全实现
- 1.1.3 无锁安全实现
- 1.2 cas工作方式
- 1.3 CAS的效率和特点
- 二、原子整数
- 三、原子引用
- 3.1 AtomicReference
- 3.2 ABA问题
- 3.3 AtomicMarkableReference
- 四、原子累加器
一、CAS原理
CAS全称CompareAndSet或者CompareAndSwap,其中【比较-交换】的操作是原子的,下面先一个具体的案例
1.1 无锁保护共享变量
定义一个接口
interface Acount {void withdraw(int amount);Integer getBalance();//设置余额为10000,一千个线程每个线程减少10,正确结果应该为0static 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;}@Overridepublic 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加锁即可
@Overridepublic 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;}@Overridepublic 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体现的是无锁并发、无阻塞并发。由于没有加锁,不会发生阻塞,从而提高效率。但是在竞争激烈的情况下,会发生大量的无效重试,反而会影响效率。
二、原子整数
AtomicInteger、AtomicBoolean、AtomicLong,这三个比较类似,都可以看做是对一个整数的封装。
public class AtomicIntegerTest {public static void main(String[] args) {AtomicInteger i = new AtomicInteger(0);//自增等价于++iSystem.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());//自己实现updateAndGetSystem.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;}
}
三、原子引用
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);}@Overridepublic void withdraw(int amount) {while (true) {BigDecimal prev = balance.get();BigDecimal next = prev.subtract(new BigDecimal(amount));if (balance.compareAndSet(prev, next)) {break;}}}@Overridepublic 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减少失败重试,最后将结果汇总,最终减少竞争提高效率。