1. 无锁解决
- CAS与volatile
- 原子整数
- 原子引用
- 原子累加器
- Unsafe
1.1 提出问题
有如下需求,保证 accounet.withdraw 取款方式的线程安全
/** * 无锁 */ public class test1 { public static void main(String[] args) { AccountUnsafe account = new AccountUnsafe(10000); Account.demo(account); } } class AccountUnsafe implements Account{ private Integer balance; public AccountUnsafe(Integer balance) { this.balance = balance; } @Override public Integer getBalance() { // 临界区 return this.balance; } @Override public void withdraw(Integer amount) { // 临界区 this.balance -= amount; } } interface Account{ Integer getBalance(); void withdraw(Integer amount); static void demo(Account account){ ArrayList<Thread> ts = new ArrayList<>(); /* 这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0 */ for (int i = 0; i < 1000; i++) { ts.add(new Thread(()->{account.withdraw(10);},"t"+i)); } long start = System.nanoTime(); ts.forEach(Thread::start); ts.forEach(t->{ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(account.getBalance()+" ——cost: "+(end-start)/100_000 +"ms"); } }
- 解决问题
在临界区的两个方法上使用synchronized修饰即可
@Override public synchronized Integer getBalance() { return this.balance; } @Override public synchronized void withdraw(Integer amount) { // 临界区 this.balance -= amount; }
1.1.1 无锁解决
其他代码不变,更改实现类
class AccountCas implements Account{ private AtomicInteger balance; public AccountCas(int balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } @Override public void withdraw(Integer amount) { while (true){ // 获取余额的最新值 int prev = balance.get(); // 要修改的金额 int next = prev - amount; // 真正修改;第一个参数:修改的值;第二个参数:更换的新值 if (balance.compareAndSet(prev, next)){ break; } } } }
1.2 CAS 与 volatile
1.2.1 CAS
AtomicInteger解决方案,内部并没有用锁来保护共享变量的线程安全。
那么它底层是如何实现无锁即线程安全呢?
public void withdraw(Integer amount) { while (true){ // 获取余额的最新值 int prev = balance.get(); // 要修改的金额 int next = prev - amount; // 真正修改;第一个参数:修改的值;第二个参数:更换的新值 if (balance.compareAndSet(prev, next)){ break; } } }
其中的关键是compareAndSet,它的简称就是CAS(Coompare And Swap),它必须是 原子操作
compareAndSet 会先进行判断,判断什么?
- 判断获得的值与共享变量是否一致
- 如果一致:返回true,并修改值
- 如果不一致:返回false,不做修改
CAS的底层使用的是:lock cmpxchg 指令(X86架构)
在单核CPU和多核CPU下都能够保证【比较-交换】的原子性
- 在多核状态下,某个核执行到带lock的指令时,CPU会让总线锁住,当这个核把此指令执行完毕,再开启总线。
在这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的
1.2.2 volatile
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰
它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
即一个线程对volatile变量的修改,对另一个线程可见
需要注意的是:volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
CAS必须借助 volatile 才能读取到共享变量的最新值来实现【compareAndSet】的效果
1.2.3 为什么无锁效率高
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
- 就如一辆开车起步熄火一样,起步又得做一套点火、挂挡等操作
- 而一直缓慢行驶则能够保持较好运行轨迹
- 无锁情况下,因为线程需要保持运行,需要额外CPU的支持,CPU在这里就好比高速跑道。
- 没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入课运行状态,还是会导致上下文切换
1.2.4 CAS的特点
结合CAS和volatile可以实现无锁并发,适用于线程数较少、多核CPU的场景下
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,会进行重试
- synchronized是基于悲观锁的思想:最悲观的估计,需要防止其他线程来修改关共享变量,只有自己将任务做完才会释放锁,让其他人使用
- CAS体现的是无锁并发、无阻塞并发。
- 因为没有使用synchronized,所以线程不会陷入阻塞,这时效率提升的因素之一
- 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响
1.3 原子对象
1.3.1 原子整数
JUC并发包中提供了
- AtomicBoolean
- AtomicInteger
- AtomicLong
基本使用都是一致的,以其中一种为例
private volatile int value; /** * Creates a new AtomicInteger with the given initial value. * * @param initialValue the initial value */ public AtomicInteger(int initialValue) { value = initialValue; }
- 基本使用
public class test2 { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(0); System.out.println(i.incrementAndGet()); // 自增并获取值 - ++i System.out.println(i.getAndIncrement()); // i++ System.out.println(i.decrementAndGet()); // --i System.out.println(i.getAndDecrement()); // i-- i.getAndAdd(5); // i+=5 i.addAndGet(5);// 先增加再赋值 System.out.println(i.get()); } }
1.3.1.1 updateAndGet() & getAndUpdate()
i.updateAndGet(value->value / 2); i.updateAndGet(value->{ int num = value * 2; return num / 4; });
方法内部是提供一个函数式接口(lambda表达式)
它所返回值是做出原子操作后的结果
其中这两个方法没有本质区别,只不过是先赋值还是先读取罢了
- 接口底层
private volatile int value; public final int updateAndGet(IntUnaryOperator updateFunction) { int prev, next; do { prev = get(); // 使用接口的抽象方法来处理更新值 next = updateFunction.applyAsInt(prev); } while (!compareAndSet(prev, next)); return next; } public final int get() { return value; } int applyAsInt(int operand); public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); } public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
底层还是使用compareAndSwapInt()方法来进行计算
1.3.2 原子引用
AtomicReferenceAtomicMarkableReferenceAtomicStampedreferenece
package com.renex.c7; import java.math.BigDecimal; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * 无锁 */ public class test3 { public static void main(String[] args) { DecimalCas account = new DecimalCas(new BigDecimal(10000)); AccountBigDecimal.demo(account); } } class DecimalCas implements AccountBigDecimal{ // 使用原子引用类型:这里引用 BigDecimal 类型 private AtomicReference<BigDecimal> balance; public DecimalCas(BigDecimal balance) { this.balance = new AtomicReference<>(balance); } @Override public BigDecimal getBalance() { return balance.get(); } @Override public void withdraw(BigDecimal amount) { while (true){ // 获取余额的最新值 BigDecimal prev = balance.get(); // 要修改的金额 BigDecimal next = prev.subtract(amount); // 真正修改;第一个参数:修改的值;第二个参数:更换的新值 if (balance.compareAndSet(prev, next)){ break; } } } } interface AccountBigDecimal{ BigDecimal getBalance(); void withdraw(BigDecimal amount); static void demo(AccountBigDecimal account){ ArrayList<Thread> ts = new ArrayList<>(); /* 这里开启1000个线程,每个线程在启动时都取走了10;最后结果输出应该是0 */ for (int i = 0; i < 1000; i++) { ts.add(new Thread(()->{account.withdraw(BigDecimal.TEN);},"t"+i)); } long start = System.nanoTime(); ts.forEach(Thread::start); ts.forEach(t->{ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(account.getBalance()+" ——cost: "+(end-start)/100_000 +"ms"); } }
其实原子对象的使用方式都基本一致
1.3.2.1 ABA问题
static AtomicReference<String> ref = new AtomicReference<String>("A"); public static void main(String[] args) throws InterruptedException { log.info("main start...."); String prev = ref.get(); other(); Thread.sleep(1000); log.info("change A->C {}",ref.compareAndSet(ref.get(),"C")); } private static void other(){ new Thread(()->{ log.info("change A->B {}",ref.compareAndSet(ref.get(),"B")); },"t1").start(); new Thread(()->{ log.info("change B->A {}",ref.compareAndSet(ref.get(),"A")); },"t2").start(); } //////////////////////////// [main] INFO test4 - main start.... [t1] INFO test4 - change A->B true [t2] INFO test4 - change B->A true [main] INFO test4 - change A->C true
无法感知其他线程对共享变量是否做出更改。
虽然不会影响运行,但是会存在一种隐患
主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况,如果主线程希望:
- 只要有其他线程【动过了】共享变量,那么它自己的cas就算失败,这时仅比较值是不够的,需要再加一个版本号
1.3.2.2 AtomicStampedreferenece
除了需要比较值的一致,还需要比较版本号一致
- 只有两者都处于一致状态,才会发生修改
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0); public static void main(String[] args) throws InterruptedException { log.info("main start...."); String prev = ref.getReference(); // 版本号 int stamp = ref.getStamp(); other(); Thread.sleep(1000); log.info("change A->C {}",ref.compareAndSet(ref.getReference(),"C",stamp,stamp+1)); System.out.println("main "+stamp); } private static void other(){ new Thread(()->{ int stamp = ref.getStamp(); log.info("change A->B {}",ref.compareAndSet(ref.getReference(),"B",stamp,stamp+1)); System.out.println("=========t1============"+stamp); },"t1").start(); new Thread(()->{ int stamp = ref.getStamp(); log.info("change B->A {}",ref.compareAndSet(ref.getReference(),"A",stamp,stamp+1)); System.out.println("===========t2=========="+stamp); },"t2").start(); } /////////////// [main] INFO test5 - main start.... [t1] INFO test5 - change A->B true [t2] INFO test5 - change B->A true =========t1============0 ===========t2==========1 main 0 [main] INFO test5 - change A->C false
main线程获得的stamp还是初始值 0,但是获取到初始值后,main线程就开始了休眠
能够看到,t1线程获得stamp版本号还是初始值0,当做完修改后版本号+1(stamp=1)
因为t2线程开始时先获取了一次stamp版本号,所以t2拿取的版本号依旧是t1线程更改后的 1
那么t2就可以判断一致,所以也进行了更改
当main线程经过休眠继续执行时,就会发现版本号不一致,那么这时候就无法继续修改
1.3.2.3 AtomicMarkableReference
AtomicStampedreferenece 可以给原子引用加上版本号,追踪原子引用整个的变化过程
通过 AtomicStampedreferenece 可以知道,引用变量在中途中被更改过几次
- 而在有些时候,并不关心引用变量更改了几次,只是单纯的关系是否被更改过,所有就有了 AtomicMarkableReference
@Slf4j(topic = "test5") public class test6 { public static void main(String[] args) throws InterruptedException { demo data = new demo("OK"); AtomicMarkableReference<demo> ref = new AtomicMarkableReference<>(data,false); log.info("start...."); demo prev = ref.getReference(); log.info("result:{}", prev.toString()); new Thread(() -> { log.info("thread start"); data.setOk("NOT OK"); boolean b = ref.compareAndSet(data, data, true, false); log.info(data +"__"+b); },"t1").start(); Thread.sleep(1000); boolean newOk = ref.compareAndSet(prev, new demo("NEW OK"), true, false); log.info("change result:{}", newOk); log.info(ref.getReference().toString()); } @AllArgsConstructor @NoArgsConstructor @Data static class demo{ private String ok; } } /////////////////////////////// [main] INFO test5 - start.... [main] INFO test5 - result:test6.demo(ok=OK) [t1] INFO test5 - thread start [t1] INFO test5 - test6.demo(ok=NOT OK)__false [main] INFO test5 - change result:false [main] INFO test5 - test6.demo(ok=NOT OK)
AtomicMarkableReference 没有那么复杂,它仅做bool值的判断,但值不一致时就判断共享变量遭遇了变动
1.3.3 原子数组
原子数组类型,这个其实和AtomicInteger等类似,只不过在修改时需要指明数组下标。
CAS是按照==来根据地址进行比较。数组比较地址,肯定是不行的,只能比较下标元素。而比较下标元素,就和元素的类型有关系了。
原子类型数组有以下四种:
AtomicIntegerArrayAtomicLongArrayAtomicReferenceArrayAtomicBooleanArray
AtomicIntegerArray 和 AtomicLongArray 的使用方式差别不大,AtomicReferenceArray因为他的参数为引用数组,所以跟前两个的使用方式有所不同。AtomicBooleanArray在生产中使用的很少。
public static void main(String[] args) { AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); for (int i = 0; i < atomicIntegerArray.length(); i++) { // 这里执行,i的目标初始值是0,因为在for循环前定义了i这个变量的值是0 // 第一个参数:更新的数组的索引;第二个参数:确认值;第三个参数:实行更改的值 boolean b = atomicIntegerArray.compareAndSet(i, 0, i + 10); System.out.println(b); } // 这里目标确认值是0是错误的,因为在for循环中已经成功更改过了,所以这里的目标确认值应是 13 System.out.println(atomicIntegerArray.compareAndSet(3, 0, 30)); // System.out.println(atomicIntegerArray.compareAndSet(3, 13, 30)); System.out.println(atomicIntegerArray.get(2)); System.out.println(atomicIntegerArray.get(3)); } /////////////////////////// true true true true true true true true true true false 12 30
1.3.4 字段更新器
AtomicReferenceFieldUpdater// 域 字段AtomicIntegerFieldUpdaterAtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合volatile修饰的字段使用,否则会出现异常
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type at java.util.concurrent.atomic.AtomicReferenceFieldUpdater$AtomicReferenceFieldUpdaterImpl.<init>(AtomicReferenceFieldUpdater.java:348) at java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater(AtomicReferenceFieldUpdater.java:110) at com.renex.c8.test2.main(test2.java:16)
示例代码:
public class test2 { public static void main(String[] args) { Student stu = new Student(); /** * newUpdater() * 第一个参数:保护的类的类型 * 第二个参数:保护的具体变量类型 * 第三个参数:修改的变量名 */ AtomicReferenceFieldUpdater<Student, String> updater = AtomicReferenceFieldUpdater .newUpdater(Student.class, String.class, "name"); /** * updater.compareAndSet() * 第一个参数:修改的对象 * 第二个参数:确定目标值 * 第三个参数:修改为什么值 */ System.out.println(updater.compareAndSet(stu, null, "zhangsan"));; System.out.println(stu); } } class Student{ volatile String name; @Override public String toString() { return "Student{" + "name='" + name + '\'' + '}'; } } ///////////////////////////// true Student{name='zhangsan'}
1.3.5 原子累加器
1.3.5.1 LongAdder 累加器性能比较
package com.renex.c8; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.LongAdder; import java.util.function.Consumer; import java.util.function.Supplier; public class test3 { public static void main(String[] args) { demo(()->new AtomicLong(0), (adder)->adder.getAndIncrement() ); ////////////// demo(()->new LongAdder(), (adder)->adder.increment() ); } /** * @param adderSupplier 累加对象 * @param action 行动抽象函数 * @param <T> */ private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action){ T adder = adderSupplier.get(); ArrayList<Thread> ts = new ArrayList<>(); for (int i = 0; i < 4; i++) { ts.add(new Thread(() -> { for (int j = 0; j < 50000; j++) { action.accept(adder); } })); } long start = System.nanoTime(); ts.forEach(Thread::start); ts.forEach(t->{ try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder+"cost: "+(end-start)/100_000+" ms"); } } ///////////////////////// 200000cost: 115 ms 200000cost: 69 ms
性能提升的原因很简单,就是在有竞争的时候,设置多个累加单元,Thread-0 累加 Cell[0],而Thread-1 累加 Cell[1]…最后将结果汇总即可
这样它们在累加时操作着不同的Cell变量,因此减少了CAS重试失败,从而提高性能
1.3.6 Unsafe
Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得
// 找到unsafe类名,通过反射找到对应的类 Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); // 不检查 Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null
1.3.7 Unsafe CAS 操作
package com.renex.c8; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import sun.misc.Unsafe; import java.lang.reflect.Field; public class test5 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { // 找到unsafe类名,通过反射找到对应的类 Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); // 不检查 Unsafe unsafe = (Unsafe) theUnsafe.get(null);// 默认就是null Field id = Student.class.getDeclaredField("id"); Field name = Student.class.getDeclaredField("name"); long idOffset = unsafe.objectFieldOffset(id); long nameOffset = unsafe.objectFieldOffset(name); Student student = new Student(); // 基本就是原子操作那一套 unsafe.compareAndSwapObject(student, idOffset, 0, 22); unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); } @AllArgsConstructor @NoArgsConstructor @Data static class Student{ volatile int id; volatile String name; } }
2. 👍JUC 专栏 - 前篇回顾👍
- (Java并发编程—JUC)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
- (Java并发编程——JUC)共享问题解决与synchronized对象锁分析!全程干货!!快快收藏!!
- (Java并发编程——JUC)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!
- (Java并发编程——JUC)从JMM内存模型的角度来分析CAS并发性问题