线程共享模型之无锁三

导读:本篇文章讲解 线程共享模型之无锁三,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录


无锁与管程的区别:

  • 管程 —- 悲观锁 —- 阻塞
  • 无锁 —- 乐观锁 —- 非阻塞

        独占锁是一种悲观锁,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<BigDecimal> 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<String> 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 <T> void demo(
     Supplier<T> arraySupplier,
     Function<T, Integer> lengthFun,
     BiConsumer<T, Integer> putConsumer,
     Consumer<T> printConsumer ) {
     List<Thread> ts = new ArrayList<>();
     T array = arraySupplier.get();
     int length = lengthFun.apply(array);
     for (int i = 0; i < length; i++) {
         // 每个线程对数组作 10000 次操作
         ts.add(new Thread(() -> {
             for (int j = 0; j < 10000; j++) {
                 putConsumer.accept(array, j%length);
             }
         }));
     }
     ts.forEach(t -> 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 <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
     T adder = adderSupplier.get();
     long start = System.nanoTime();
     List<Thread> ts = new ArrayList<>();
     // 4 个线程,每人累加 50 万
     for (int i = 0; i < 40; i++) {
         ts.add(new Thread(() -> {
             for (int j = 0; j < 500000; j++) {
                 action.accept(adder);
             }
         }));
     }
     ts.forEach(t -> 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 < 5; i++) {
     demo(() -> new AtomicLong(), adder -> adder.getAndIncrement());
}
for (int i = 0; i < 5; i++) {
     demo(() -> 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 操作:

@Data
class 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); // 返回 true
UnsafeAccessor.unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
System.out.println(student);

输出:

Student(id=20, name=张三)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/2145.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!