共享模型之无锁并发
问题案例:账户取款
- 通过取款案例可知,多线程取款时对Integer读写操作存在线程安全性问题
- 原因:可对字节码分析,多线程执行时,可能导致指令交替执行
解决方案
-
有锁
- 加synchronized,可保证多线程下共享变量的原子性,前面已经详细讲过
-
无锁
- 使用AtomicInteger原子整型类的CAS机制
- 性能在线程竞争不激烈时,相比加锁性能高很多
-
代码案例
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
if (balance.compareAndSet(prev, next)) {
break;
}
}
// 可以简化为下面的方法
// balance.addAndGet(-1 * amount);
}
CAS与volatile
-
cas需要volatile的支持,底层关键指令:lock cmpxchg(X86 架构),CPU执行此类指令将锁住总线,待执行完毕后解锁,过程中不会被线程调度机制打断,保证其原子性
-
其元子类内部的value属性上存在volatile修饰
-
cas相比加锁,CPU高速运行,但没有上下文切换,无锁无阻塞
-
类比高速跑道上的赛车,高速运行速度快,但一旦要停下来再跑,就需要经过减速、熄火、启动、加速等等过程,才会恢复高速,代价很大
-
适合线程数较少,且多核CPU上性能发挥更佳,线程竞争激烈,重试过多,性能可能不佳
-
CAS属于乐观锁思想
- 乐观:不怕别人来改,改了我就重试嘛
- 悲观:得先烦着别人来改,我先上锁,你们都不要进来
原子整数
-
包括
- AtomicBoolean
- AtomicInteger
- AtomicLong
-
代码案例
# TODO
原子引用
- AtomicReference(普通引用)
- 存在ABA问题:一个共享变量被来回修改无法感知,一般情况下对业务没有影响
- 解决方案1:AtomicStampedReference(比较值+version)
- 解决方案2:AtomicMarkableReference(只关心是否被改了)
- 案例:主人加保洁阿姨换垃圾袋
原子数组
- 思考:普通数组,其内部元素是否安全?
- 案例代码:lambda 实现多线程对数组元素进行累加,存在问题
- 解决方案:AtomicIntegerArray
- 代码案例
/**
* 对比普通数组与原子数组在多线程下的安全性
* @date 2021年8月1日
* @author qinchen
*/
public class TestAtomicIntegerArray {
public static void main(String[] args) {
// 普通数组,不能保证数组中的元素的线程安全
demo(
// 创建一个长度为10的数组
() -> new int[10],
// 通过输入的类型参数(1已决定是数组),获取数组长度
(array) -> array.length,
// 通过输入的数组及索引,让数组元素自增
(array, index) -> array[index]++,
// 打印数组
array -> System.out.println(Arrays.toString(array))
);
// 原子数组,可以保证数组中元素操作的原子性
demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
}
private static <T> void demo(
Supplier<T> arraySuppler,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer) {
// 创建一个 list 存放创建的所有线程
List<Thread> ts = new ArrayList<>();
// 取到输入的对象
T array = arraySuppler.get();
// 将对象作为输入参数,返回一个Integer
int length = lengthFun.apply(array);
// 根据 length,遍历创建 length 个 thread
for (int i = 0; i < length; i++) {
ts.add(new Thread(() -> {
// 每个线程中,对 putConsumer 做 10000 次运算
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j % length);
}
}));
}
ts.forEach(Thread::start);
// 等所有线程执行完毕
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印 10 个线程 最后对 数组元素进行累加后的结果
printConsumer.accept(array);
}
}
原子更新器
- AtomicReferenceFieldUpdater
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- 可原子更新某个对象的某个域(可以理解为字段),需配合volatile关键字使用
- 代码案例
/**
* 对象属性的原子更新器
*/
public class TestAtomicReferenceFieldUpdater {
public static void main(String[] args) {
Student student = new Student();
AtomicReferenceFieldUpdater<Student, String> updater =
AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
boolean b = updater.compareAndSet(student, null, "张三");
if(b) {
System.out.println(student);
}
}
}
class Student {
/**
* 注意:必须是 volatile 类型
*/
volatile String name;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
原子累加器
-
特点
- LongAdder相比AtomicLong的.getAndIncrement,其性能更高
- 其原因主要因为内部设置了多个累加单元,各线程自行先累加,最后在汇总,总分总思想
- 这样减少了CAS失败重试的概率,从而提升了性能
-
代码案例
/**
* 累加器,性能比使用原子类更好
* 原理:线程有竞争时,设置多个【累加单元】,对应不同线程
* 最后将多个【累加单元】进行汇总,这样累加操作不同的单元变量,减少了CAS重试失败频率
* 从而提升了性能
*/
public class TestLongAdder {
public static void main(String[] args) {
// 想对较慢
demo(() -> new AtomicLong(0),
(addr) -> addr.getAndIncrement());
// 性能更好
demo(() -> new LongAdder(),
(addr) -> addr.increment());
}
private static <T> void demo(Supplier<T> adderSupplier, Consumer<T> action) {
T t = adderSupplier.get();
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 4; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(t);
}
}));
}
long start = System.nanoTime();
ts.forEach(Thread::start);
ts.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.nanoTime();
System.out.println(t + " cost = " + (end - start) / 1000_000 + "ms");
}
}
-
源码解析
-
来自:Doug Lea ,设计非常精巧
-
内部关键域
- volatile Cell[] cells 累加单元
- volatile long base 没有竞争,CAS用此域
- volatile int cellsBusy 创建或扩容时的标记
-
缓存行伪共享问题
- @sun.misc.Contended 防止缓存行的伪共享
- 涉及到CPU三级缓存与内存
- 缓存以缓存行为单位,一般为64byte
- 缓存会导致数据产生多份副本,一份数据可能缓存在多个核心的缓存行中
- CPU要保证数据一致性,需在某个核心修改数据后,其他核对应的缓存行必须失效
- @sun.misc.Contended标注的对象前后各家128字节大小的padding,目的是让其占用不同核心缓存行
-
Unsafe
-
特点
- 非常底层的操作内存线程的方法
- 不能直接调用,需要进行反射调用
- 名字提醒开发者一般不建议直接使用,误用容易出现一些对系统不安全因素
-
代码案例
public class TestUnsafe {
/**
* Unsafe 必须通过反射方式获取到该类对象
*
* @param args
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
System.out.println(unsafe);
// 下面通过 unsafe 修改对象属性,保证线程安全
Teacher teacher = new Teacher();
// 1. 获得一个域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
// 2. 执行 cas 操作
unsafe.compareAndSwapInt(teacher, idOffset, 0, 1);
unsafe.compareAndSwapObject(teacher, nameOffset, null, "张三");
System.out.println(teacher);
}
}
@Data
class Teacher {
volatile int id;
volatile String name;
}