线程共享模型----之----无锁(三)
目录
线程共享模型总目录
3.1 CAS
3.2 原子整数
3.3 原子引用
3.4 原子数组
3.5 字段更新器
3.6 原子累加器
3.7 Unsafe
无锁与管程的区别:
- 管程 ---- 悲观锁 ---- 阻塞
- 无锁 ---- 乐观锁 ---- 非阻塞
独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁,而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。CAS 就是一种乐观锁。
3.1 CAS
CAS, compare and swap 的缩写,中文翻译成比较并交换。是一种通过无锁的方式来保护共享变量线程安全问题的机制。其实现可如下:(其中 balance 是用 AtomicInteger 修饰的变量)
/* 将初始余额1000元, 每次 -10 元, 如果启动100个线程, 则余额应该变成0元, 以下是减一次的操作*/public void withdraw(Integer amount) { while(true) { // 需要不断尝试,直到成功为止 while (true) { // 比如拿到了旧值 1000 int prev = balance.get(); // 在这个基础上 1000-10 = 990 int next = prev - amount; /* compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值 - 不一致了,next 作废,返回 false 表示失败 比如,别的线程已经做了减法,当前值已经被减成了 990 那么本线程的这次 990 就作废了,进入 while 下次循环重试 - 一致,以 next 设置为新值,返回 true 表示成功 */ if (balance.compareAndSet(prev, next)) { break; } } }}
其中的关键是 compareAndSet,它的简称就是 CAS,它必须是原子操作。其底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证比较-交换的原子性。
CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
volatile 回顾:
获取共享变量时,为了保证该变量的可见性,需要使用 volatile 修饰。它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。即一个线程对 volatile 变量的修改,对另一个线程可见。但需要注意的是:volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(即不能保证原子性)
当线程数不多于cpu核心数时,无锁的效率要高于加锁的效率,这是为什么呢?
原因是,无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
打个比喻,线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换,所以在线程数少于cpu核心数时,无锁的效率就会更高。
3.2 原子整数
J.U.C 并发包提供了:
- AtomicBoolean
- AtomicInteger
- AtomicLong
以 AtomicInteger 为例:(其中 i 是用 AtomicInteger 修饰的变量)
AtomicInteger i = new AtomicInteger(0);
// 获取并自增( i = 0, 结果 i = 1, 返回 0 ),类似于 i++ System . out . println ( i . getAndIncrement ()); // 自增并获取( i = 1, 结果 i = 2, 返回 2 ),类似于 ++i System . out . println ( i . incrementAndGet ()); // 自减并获取( i = 2, 结果 i = 1, 返回 1 ),类似于 --i System . out . println ( i . decrementAndGet ()); // 获取并自减( i = 1, 结果 i = 0, 返回 1 ),类似于 i-- System . out . println ( i . getAndDecrement ()); // 获取并加值( i = 0, 结果 i = 5, 返回 0 ) System . out . println ( i . getAndAdd ( 5 )); // 加值并获取( i = 5, 结果 i = 0, 返回 0 ) System . out . println ( i . addAndGet ( - 5 )); // 获取并更新( i = 0, p 为 i 的当前值 , 结果 i = -2, 返回 0 ) // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . getAndUpdate ( p -> p - 2 )); // 更新并获取( i = -2, p 为 i 的当前值 , 结果 i = 0, 返回 0 ) // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . updateAndGet ( p -> p + 2 )); // 获取并计算( i = 0, p 为 i 的当前值 , x 为参数 1, 结果 i = 10, 返回 0 ) // 其中函数中的操作能保证原子,但函数需要无副作用 // getAndUpdate 如果在 lambda 中引用了外部的局部变量,要保证该局部变量是 final 的 // getAndAccumulate 可以通过 参数 1 来引用外部的局部变量,但因为其不在 lambda 中因此不必是 final System . out . println ( i . getAndAccumulate ( 10 , ( p , x ) -> p + x )); // 计算并获取( i = 10, p 为 i 的当前值 , x 为参数 1, 结果 i = 0, 返回 0 ) // 其中函数中的操作能保证原子,但函数需要无副作用 System . out . println ( i . accumulateAndGet ( - 10 , ( p , x ) -> p + x ));
3.3 原子引用
原子引用类型有哪些?
- AtomicReference
- AtomicStampedReference
- AtomicMarkableReference
【AtomicReference】
(其中 balance 是用 AtomicReference 修饰的变量)
/* 将初始余额1000元, 每次 -10 元, 如果启动100个线程, 则余额将会变成0元*/class DecimalAccountSafeCas implements DecimalAccount { AtomicReference balance; public DecimalAccountSafeCas(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 (ref.compareAndSet(prev, next)) { break; } } }}
使用 AtomicReference 修饰的变量,仅能够判断共享变量的值是否与期盼的值相同,无法判断此共享变量之前是否被修改过,如果当前操作共享变量的线程希望:只要有其他线程动过了共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号,AtomicStampedReference。
【AtomicStampedReference】
// 第二个参数即为当前的版本号static AtomicStampedReference ref = new AtomicStampedReference("A", 0);public static void main(String[] args) throws InterruptedException { log.debug("main start..."); String prev = ref.getReference(); // 获取值 A int stamp = ref.getStamp(); // 获取版本号 log.debug("版本 {}", stamp); other(); // 如果中间有其它线程干扰,发生了 ABA 现象 sleep(1); // 尝试改为 C log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));}// 线程干扰, 即在主线程修改共享变量之前, 将共享变量的值修改为其他值后再修改回来, 使版本号变化private static void other() { new Thread(() -> { log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B", ref.getStamp(), ref.getStamp() + 1)); log.debug("更新版本为 {}", ref.getStamp()); }, "t1").start(); sleep(0.5); new Thread(() -> { log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A", ref.getStamp(), ref.getStamp() + 1)); log.debug("更新版本为 {}", ref.getStamp()); }, "t2").start();}
【AtomicMarkableReference】
AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: A - > B - > A - > C ,通过 AtomicStampedReference ,我们可以知道,引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过 ,所以就有了AtomicMarkableReference(第二参数为布尔类型)
3.4 原子数组
原子数组有哪些?
- AtomicIntegerArray
- AtomicLongArray
- AtomicReferenceArray
有如下方法:创建10个线程,每个线程分别对数组操作(自增)10000次(采用了函数式编程)
/ 参数1,提供数组、可以是线程不安全数组或线程安全数组 参数2,获取数组长度的方法 参数3,自增方法,回传 array, index 参数4,打印数组的方法*/// supplier 提供者 无中生有 ()->结果// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->private static void demo( Supplier arraySupplier, Function lengthFun, BiConsumer putConsumer, Consumer printConsumer ) { List ts = new ArrayList(); T array = arraySupplier.get(); int length = lengthFun.apply(array); for (int i = 0; i { for (int j = 0; j t.start()); // 启动所有线程 ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); // 等所有线程结束 printConsumer.accept(array);}
不安全的数组:
demo( ()->new int[10], (array)->array.length, (array, index) -> array[index]++, array-> System.out.println(Arrays.toString(array)));
结果:
[9870, 9862, 9774, 9697, 9683, 9678, 9679, 9668, 9680, 9698]
安全的数组:
demo( ()-> new AtomicIntegerArray(10), (array) -> array.length(), (array, index) -> array.getAndIncrement(index), array -> System.out.println(array));
结果:
[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
3.5 字段更新器
- AtomicReferenceFieldUpdater // 域 字段
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
利用字段更新器,可以针对对象的某个域( Field )进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常
加 volatile :
3.6 原子累加器
- LongAdder
private static void demo(Supplier adderSupplier, Consumer action) { T adder = adderSupplier.get(); long start = System.nanoTime(); List ts = new ArrayList(); // 4 个线程,每人累加 50 万 for (int i = 0; i { for (int j = 0; j t.start()); ts.forEach(t -> { try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); } }); long end = System.nanoTime(); System.out.println(adder + " cost:" + (end - start)/1000_000);}
比较 AtomicLong 与 LongAdder :
for (int i = 0; i new AtomicLong(), adder -> adder.getAndIncrement());}for (int i = 0; i new LongAdder(), adder -> adder.increment());}
输出:
可以看到使用 LongAdder 累加器性能明显提升。性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加 Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了CAS 重试失败,从而提高性能。
3.7 Unsafe
Unsafe 对象提供了非常底层的、操作内存、线程的方法, Unsafe 对象不能直接调用,只能通过反射获得
public class UnsafeAccessor { static Unsafe unsafe; static { try {Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); unsafe = (Unsafe) theUnsafe.get(null); } catch (NoSuchFieldException | IllegalAccessException e) { throw new Error(e); } } static Unsafe getUnsafe() { return unsafe; }}
Unsafe CAS 操作:
@Dataclass Student { volatile int id; volatile String name; }
Unsafe unsafe = UnsafeAccessor.getUnsafe();Field id = Student.class.getDeclaredField("id");Field name = Student.class.getDeclaredField("name");// 获得成员变量的偏移量long idOffset = UnsafeAccessor.unsafe.objectFieldOffset(id);long nameOffset = UnsafeAccessor.unsafe.objectFieldOffset(name);Student student = new Student();// 使用 cas 方法替换成员变量的值UnsafeAccessor.unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 trueUnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 trueSystem.out.println(student);
输出:
Student(id=20, name=张三)