并发编程(六):悲观锁与乐观锁、atomic常见类、Unsafe对象
本文目录
一、悲观锁与乐观锁
1.synchronized保证线程安全
//使用synchronized
public class Test1 {
public static void main(String[] args) {
Account.demo(new AccountSafe(10000));
//Account.demo(new AccountCas(10000));
}
}
class AccountSafe implements Account {
private Integer balance;
public AccountSafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this) {
return balance;
}
}
@Override
public void withdraw(Integer amount) {
// 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
synchronized (this) {
balance -= amount;
}
}
}
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
//启动1000个线程,每个线程取款10元
List<Thread> ts = new ArrayList<>();
long start = System.nanoTime();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(10);
}));
}
ts.forEach(thread -> 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) / 1000_000 + " ms");
}
}
上面的代码中使用synchronized加锁(悲观锁)操作来保证线程安全,但是synchronized加锁操作太耗费资源,这里我们使用无锁(乐观锁)来解决此问题
class AccountCas implements Account {
//使用原子整数: 底层使用CAS+重试的机制
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;
//比较并设置值
/*
此时的prev为共享变量的值, 如果prev被别的线程改了.也就是说: 自己读到的共享变量的值 和 共享变量最新值 不匹配,
就继续where(true),如果匹配上了, 将next值设置给共享变量.
AtomicInteger中value属性, 被volatile修饰, 就是为了确保线程之间共享变量的可见性.
*/
if(balance.compareAndSet(prev, next)) {
break;
}
}
}
}
使用原子操作来保证线程访问共享资源的安全性, cas+重试的机制来确保(乐观锁思想), 相对于悲观锁思想的synchronized,reentrantLock来说, cas的方式效率会更好。
2.CAS保证线程安全的原理
- 前面看到的AtomicInteger的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
@Override
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while (true){
// 比如拿到了旧值 100
int prev = balance.get();
// 在这个基础上 100-10 = 90
int next = prev - amount;
//比较并设置
if (atomicInteger.compareAndSet(prev,next)){
break;
}
}
}
当一个线程要去修改Account对象中的值时,先获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prev与Account中的余额进行比较。
- 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
- 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。
- 其中关键的三个操作为:
A.compareAndSet:原子操作
B.while(true):整体语句非原子,用此判断重试
C.volatile(修饰原子类中的value):CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果
3.CAS底层原理
- 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的 原子性。
- 在多核状态下,某个核执行到带 lock 的指令时,CPU 会让总线锁住,当这个核把此指令执行完毕,再开启总线,这个过程中不会被线程的调度机制所打断, 保证了多个线程对内存操作的准确性,是原子的。
4. CAS的特点
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,重试就行。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
CAS 体现的是无锁并发、无阻塞并发:
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响
二、atomic常见类
java.util.concurrent.atomic并发包提供了一些并发工具类,其均借助CAS和volatile实现乐观锁并发:
1.基本数据类型的原子类
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
上面三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。
public static void main(String[] args) {
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)
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));
}
以updateAndGet为例:
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(6);
updateAndGet(i, new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
return operand / 2; //6/2
}
});
System.out.println(i.get()); // i==3
}
public static void updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
while (true) {
int prev = i.get(); // 6
int next = operator.applyAsInt(prev); //传入一个接口(内部带一个方法),自定义实现update内容
if (i.compareAndSet(prev, next)) {
break;
}
}
}
- 调用updateAndGet方法, 将共享变量i, IntUnaryOperator对象传递过去
- 传过来的operator对象, 调用IntUnaryOperator中的applyAsInt方法, 实际调用的就是传递过来的方法, 进行除法操作。
2.引用类型的原子类
原子引用的作用: 保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人。除了基本类型之外的,也能保证其线程安全。
- AtomicReference
- AtomicMarkableReference
- AtomicStampedReference
例子:使用原子引用实现BigDecimal存取款的线程安全:
//线程不安全的实现
class DecimalAccountUnsafe implements DecimalAccount {
BigDecimal balance;
public DecimalAccountUnsafe(BigDecimal balance) {
this.balance = balance;
}
@Override
public BigDecimal getBalance() {
return balance;
}
@Override
public void withdraw(BigDecimal amount) {
BigDecimal balance = this.getBalance();
this.balance = balance.subtract(amount);
}
}
//线程安全的实现
class DecimalAccountCas implements DecimalAccount {
//原子引用,泛型类型为BigDecimal类型
private final AtomicReference<BigDecimal> balance;
public DecimalAccountCas(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;
}
}
}
}
(1)ABA问题
如果存在共享变量修改为从A修改为B,又从B修改为A,这时候仅通过判断共享变量值是否和预期值相同 无法确定是否被修改过。
(2)例子
public class Test1 {
static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) {
new Thread(() -> {
String pre = ref.get();
System.out.println("change");
try {
other();
} catch (InterruptedException e) {
e.printStackTrace();
}
Sleeper.sleep(1);
//把ref中的A改为C
System.out.println("change A->C " + ref.compareAndSet(pre, "C"));
}).start();
}
static void other() throws InterruptedException {
new Thread(() -> {
// 此时ref.get()为A,此时共享变量ref也是A,没有被改过, 此时CAS
// 可以修改成功, B
System.out.println("change A->B " + ref.compareAndSet(ref.get(), "B"));
}).start();
Thread.sleep(500);
new Thread(() -> {
// 同上, 修改为A
System.out.println("change B->A " + ref.compareAndSet(ref.get(), "A"));
}).start();
}
}
(3)使用AtomicStampedReference
public class Test1 {
//指定版本号
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) {
new Thread(() -> {
String pre = ref.getReference();
//获得版本号
int stamp = ref.getStamp(); // 此时的版本号还是第一次获取的
System.out.println("change");
try {
other();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//把ref中的A改为C,并比对版本号,如果版本号相同,就执行替换,并让版本号+1
System.out.println("change A->C stamp " + stamp + ref.compareAndSet(pre, "C", stamp, stamp + 1));
}).start();
}
static void other() throws InterruptedException {
new Thread(() -> {
int stamp = ref.getStamp();
System.out.println("change A->B stamp " + stamp + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
}).start();
Thread.sleep(500);
new Thread(() -> {
int stamp = ref.getStamp();
System.out.println("change B->A stamp " + stamp + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
}).start();
}
}
(4)使用AtomicMarkableReference
- 标记cas的共享变量是否被修改过,AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,我们可以知道,引用变量中途被更改了几次。
- 但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference
- AtomicStampedReference 需要我们传入 整型变量 作为版本号,来判定是否被更改过
- AtomicMarkableReference需要我们传入布尔变量 作为标记,来判断是否被更改过
3.字段更新器
保证多线程访问同一个对象的成员变量时, 成员变量的线程安全性。
- AtomicIntegerFieldUpdater —整形的属性
- AtomicLongFieldUpdater —长整形的属性
- AtomicReferenceFieldUpdater —引用类型的属性
注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。
@Slf4j(topic = "guizy.AtomicFieldTest")
public class AtomicFieldTest {
public static void main(String[] args) {
Student stu = new Student();
// 获得原子更新器
// 泛型
// 参数1 持有属性的类 参数2 被更新的属性的类
// newUpdater中的参数:第三个为属性的名称
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
// 期望的为null, 如果name属性没有被别的线程更改过, 默认就为null, 此时匹配, 就可以设置name为张三
System.out.println(updater.compareAndSet(stu, null, "张三"));
System.out.println(updater.compareAndSet(stu, stu.name, "王五"));
System.out.println(stu);
}
}
class Student {
volatile String name;
//String name; 会抛异常
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
//不加volatile的结果:
Exception in thread "main" java.lang.IllegalArgumentException: Must be volatile type
//加volatile后打印的结果
true
true
Student{name='王五'}
三、Unsafe对象
AtomicInteger以及其他的原子类, 底层都使用的是Unsafe类。
1.获取Unsafe对象
Unsafe 对象提供了非常底层的操作内存、线程的方法,Unsafe 对象不能直接调用,可以通过反射获得。
public class UnsafeAccessor {
private static final Unsafe unsafe;
static {
try {
//获取字段
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//设置可访问
theUnsafe.setAccessible(true);
//获取Unsafe对象
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
//封装的获取Unsafe对象的方法
public static Unsafe getUnsafe() {
return unsafe;
}
}
2.Unsafe对象的CAS
public class TestUnsafe {
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);
System.out.println(unsafe);
// 1. 获取域的偏移地址
long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));
Teacher t = new Teacher();
// 2. 执行 cas 操作
unsafe.compareAndSwapInt(t, idOffset, 0, 20);
unsafe.compareAndSwapObject(t, nameOffset, null, "张三");
// 3. 验证
System.out.println(t);
}
}
@Data
class Teacher {
volatile int id;
volatile String name;
}
//输出结果
Student(id=20, name=张三)
3.自定义简易AtomicInteger类
@Slf4j(topic = "c.Test42")
public class Test42 {
public static void main(String[] args) {
Account.demo(new MyAtomicInteger(10000));
}
}
class MyAtomicInteger implements Account {
private volatile int value;
private static final long valueOffset;
private static final Unsafe UNSAFE;
static {
UNSAFE = UnsafeAccessor.getUnsafe();
try {
valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public int getValue() {
return value;
}
public void decrement(int amount) {
while(true) {
int prev = this.value;
int next = prev - amount;
if (UNSAFE.compareAndSwapInt(this, valueOffset, prev, next)) {
break;
}
}
}
public MyAtomicInteger(int value) {
this.value = value;
}
@Override
public Integer getBalance() {
return getValue();
}
@Override
public void withdraw(Integer amount) {
decrement(amount);
}
}
四、总结
暂无