目录
无锁与管程的区别:
- 管程 —- 悲观锁 —- 阻塞
- 无锁 —- 乐观锁 —- 非阻塞
独占锁是一种悲观锁,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 原子整数
- 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
),类似于
++iSystem
.
out
.
println
(
i
.
incrementAndGet
());//
自减并获取(
i = 2,
结果
i = 1,
返回
1
),类似于
–iSystem
.
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
中因此不必是
finalSystem
.
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】
可以给原子引用加上版本号,追踪原子引用整个的变化过程,如: 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
对象不能直接调用,只能通过反射获得
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