- 问题引出
- 底层原理概述—->CAS
- volatile 在 CAS中的作用
- 为什么CAS+重试(无锁)效率高
- 乐观锁和悲观锁的特点)">CAS 的特点 (乐观锁和悲观锁的特点)
- volatile 在 CAS中的作用
- 原子类
- CAS底层原理
- Java中 synchronized 和 ReentrantLock 等 独占锁就是
**悲观锁**
思想的体现 。 - 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用基于
乐观锁
思想的**CAS**
方式实现的 。 - 管程即
monitor
是阻塞式的悲观锁
实现并发控制
,这章我们将通过非阻塞式的乐观锁
的来实现并发控制
。
问题引出
使用synchronized等加锁的方式实现线程安全
- 有如下需求,保证
account.withdraw取款方法
的线程安全, 下面使用synchronized
保证线程安全
Account接口中有两个接口方法,跟一个测试方法,
interface Account {
// 获取余额
Integer getBalance();
// 取款
void withdraw(Integer amount);
/**
* Java8之后接口新特性, 可以添加默认方法
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(Account account) {
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");
}
}
实现
class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
synchronized (this) {
return balance;
}
}
@Override
public void withdraw(Integer amount) {
// 通过这里加锁就可以实现线程安全,不加就会导致线程安全问题
synchronized (this) {
balance -= amount;
}
}
}
测试
demo方法是静态的,需要接口来调,因为它就是声明在接口的。
public class Test1 {
public static void main(String[] args) {
Account.demo(new AccountUnsafe(10000));
}
}
无锁的方式实现线程安全
上面的代码中使用synchronized加锁操作来保证线程安全,但是 synchronized加锁操作太耗费资源 (因为底层使用了操作系统mutex指令, 造成内核态和用户态的切换),这里我们通过原子整型来使用 无锁 思想来解决此问题
class AccountCas implements Account {
//使用原子整数类型: 底层使用CAS+重试的机制,而不是传统的Integer整数类型
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、next都是(局部变量)存储在工作内存中的,还没有同步到主内存中
*/
//真正修改,第一个参数是调用get方法获得的值,第二个参数是要修改的值,修改成功会返回true
if(balance.compareAndSet(prev, next)) {
break;
}
}
}
}
底层原理概述—->CAS
上面使用AtomicInteger的案例解决方法,内部并没有用锁也能实现共享变量的线程安全。那么它是如何实现的呢?
其中的关键是 compareAndSwap方法(比较并设置值),它的简称就是 CAS (也有 Compare And Swap 的说法),它必须是原子操作。
我们来分析一下它具体是怎么实现的:
流程 :
- 当一个线程要去修改Account对象中的值时,先调用get方法获取值prev(),然后再将其设置为新的值next(调用cas方法)。在调用cas方法时,会将prev与Account中的余额进行比较(满足一定条件才能更新成功)。
- 如果两者相等,就说明该值还未被其他线程修改,此时便可以进行修改操作。
- 如果两者不相等,就不设置值,重新获取值prev(调用get方法),然后再将其设置为新的值next(调用cas方法),直到修改成功为止。
- 假如在执行cas方法之前,另外的一个线程就已经将其修改为90;你就得在其他线程修改的基础上再进行修改(人家修改为90,你就得在90的基础上进行你的增减操作)。既然其他线程修改了,所以你之前调用的get方法获取的prev跟共享变量上的最新结果不一样,所以本次修改失败,返回false;再次进行循环,如此反复直到修改成功。
- cas方法是怎么知道共享变量被其他线程修改的?比较的结果不一样呗
注意 :
- 其实 CAS 的底层是 lock cmpxchg 指令(X86 架构),在单核 CPU 和多核 CPU 下都能够保证【比较-交换】的原子性。
- 在多核状态下,某个核执行到带 lock 前缀的指令时,CPU 会让总线锁住,当当前这个核(线程)把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
volatile 在 CAS中的作用
- 在上面代码中的AtomicInteger类,保存值的value属性使用了volatile 修饰。获取共享变量时,为了保证该共享变量的可见性,需要使用 volatile 修饰。
- volatile可以用来修饰 成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。这就是可见性的体现,即一个线程对 volatile 变量的修改,对另一个线程可见。
- 注意: volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
**CAS 必须借助 volatile 才能读取到共享变量的最新值来实现【比较并交换】的效果**
为什么CAS+重试(无锁)效率高
使用CAS+重试—-也就是无锁模式的情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
- 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行状态,代价比较大。
但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片**,仍然会进入可运行状态,还是会导致上下文切换。**
CAS 的特点 (乐观锁和悲观锁的特点)
结合 CAS 和 volatile 可以实现无锁并发,适用于线程数少、多核 CPU 的场景下。
CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我吃亏点再重试呗。
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
- CAS 体现的是无锁并发、无阻塞并发,请仔细体会这两句话的意思
- 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一
- 但如果竞争激烈(写操作多),可以想到重试必然频繁发生,反而效率会受影响
原子类
基本类
java.util.concurrent.atomic并发包提供了一些并发工具类:使用原子操作的方式 (共享数据为基本数据类型原子类)
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
通过观察源码可以发现这几个类内部都是通过cas的原理来实现的,而cas底层又是使用到了,sun.misc.Unsafe类来实现;换句话说CAS的底层是sun.misc.Unsafe类,这里先不说这个类,后面会具体分析到它,先来看看这几个原子基本类;
这三个类提供的方法几乎相同,所以我们将以 AtomicInteger为例子来介绍。先讨论原子整数类,以 AtomicInteger 为例讨论它的api方法
AtomicInteger API演示
public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(0);
// 先获取再自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
System.out.println("i.getAndIncrement() "+i.getAndIncrement() );
System.out.println("i "+i );
// 先自增再获取(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));
}
updateAndGet的实现
上面演示的方法都只能做一些简单的加法减法,这里我们演示一下稍微更复杂一点的操作,比如除法或者乘法;
简单写法:
public static void main(String[] args) {
//想要在5的基础上进行乘法运算
AtomicInteger i = new AtomicInteger(5);
//简单写法
i.updateAndGet(value ->value * 10);
System.out.println(i.get()); // 2
}
想做更复杂的运算的话
步骤:
- 调用updateAndGet方法, 将共享变量i,以及 IntUnaryOperator对象传递过去
- updateAndGet方法内部, 传过来的operator对象, 调用IntUnaryOperator中的applyAsInt方法, 实际调用的就是传递过来的对象的方法, 进行 / 操作 ```java public static void main(String[] args) {
AtomicInteger i = new AtomicInteger(5);
updateAndGet(i, new IntUnaryOperator() {
@Override
public int applyAsInt(int operand) {
return operand / 2;
}
});
System.out.println(i.get()); // 2
}
/**
* 如果让我们来实现线程安全计算的话该如何?
* 这里我们考虑使用最基本的compareAndSet思想来实现;
* 使用一个while(true)循环来实现,一旦有其他线程将其修改,就进行重试
* 直到期待值跟最新值相等
* @param i
* @param operator
*/
public static void updateAndGet(AtomicInteger i, IntUnaryOperator operator) {
while (true) {
//获取到当前值
int prev = i.get(); // 5
//进行计算,并将结果存放在next中
int next = operator.applyAsInt(prev);
//使用compareAndSet方法进行比较并更新
if (i.compareAndSet(prev, next)) {
//若果成功了就break
break;
}
}
}
<a name="iJjVQ"></a>
### 原子引用 (AtomicReference)类
原子引用的作用: **保证引用类型的共享变量是线程安全的(确保这个原子引用没有引用过别人**
- 为什么需要原子引用类型 ? (引用数据类型原子类)
基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
- AtomicReference:引用类型原子类
- AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,**可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。**
- AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以**解决使用 CAS 进行原子更新时可能出现的 ABA 问题**。
例子 : 使用原子引用实现BigDecimal存取款的[线程安全](https://so.csdn.net/so/search?q=%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8&spm=1001.2101.3001.7020):
```java
interface DecimalAccount {
// 获取余额
BigDecimal getBalance();
// 取款
void withdraw(BigDecimal amount);
/**
* 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
* 如果初始余额为 10000 那么正确的结果应当是 0
*/
static void demo(DecimalAccount account) {
List<Thread> ts = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
ts.add(new Thread(() -> {
account.withdraw(BigDecimal.TEN);
}));
}
ts.forEach(Thread::start);
ts.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(account.getBalance());
}
}
下面这个是不安全的实现过程:
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);
}
}
解决代码如下:在AtomicReference类中,存在一个value类型的变量,保存对BigDecimal对象的引用。
public class Test1 {
public static void main(String[] args) {
DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
}
}
class DecimalAccountCas implements DecimalAccount {
//原子引用,泛型类型为小数类型,保护BigDecimal
private final AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
//创建的是一个AtomicReference
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;
}
}
}
}
ABA 问题及解决 (重点
)
如下程序所示,虽然 在other方法中存在两个线程对共享变量进行了修改,**但是修改之后又变成了原值,main线程对`修改过共享变量的过程`是不可见的,这种操作这对业务代码并无影响。** ```java public class Test1 { //该变量的初始值是A,我们想在后面将其修改为C static AtomicReference
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();
} }

- 主线程仅能判断出共享变量的值与最初值 A是否相同,不能感知到这种从 A 改为 B 又改回 A 的情况,也就是说它不能判断出其他线程将A修改成另外的值然后又修改回A值的情况,在A变为B但没变回A之前,其他线程能做很多事情了!!!
- 如果主线程希望:**只要有其它线程【修改过】共享变量,那么自己的 cas 就算失败,这时,仅比较值是不够的,需要再加一个版本号。可以使用AtomicStampedReference来解决。**
<a name="TrOYx"></a>
#### AtomicStampedReference (加版本号解决ABA问题)
- 谁修改过原来的值,就得将版本号进行更新,这样即可解决ABA问题
```java
class Test1 {
//指定版本号初始为0
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();
}
}
AtomicMarkableReference (标记cas的共享变量是否被修改过)
- AtomicStampedReference 可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A -> B -> A ->C,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了几次。
- 但是有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference ,用一个布尔值来记录是否被改动过
@Slf4j(topic = "guizy.TestABAAtomicMarkableReference")
class TestABAAtomicMarkableReference {
public static void main(String[] args) throws InterruptedException {
GarbageBag bag = new GarbageBag("装满了垃圾");
// 参数2 mark 可以看作一个标记,true表示垃圾袋满了
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("主线程 start...");
GarbageBag prev = ref.getReference();
log.debug(prev.toString());
new Thread(() -> {
log.debug("打扫卫生的线程 start...");
bag.setDesc("空垃圾袋");
// 两个都是bag没有真正进行修改,只是将状态从true改成false
while (!ref.compareAndSet(bag, bag, true, false)) {
}
log.debug(bag.toString());
}).start();
Thread.sleep(1000);
log.debug("主线程想换一只新垃圾袋?");
boolean success = ref.compareAndSet(prev, new GarbageBag("空垃圾袋"), true, false);
log.debug("换了么?" + success);
log.debug(ref.getReference().toString());
}
}
class GarbageBag {
//对垃圾的描述
String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return super.toString() + " " + desc;
}
}
AtomicStampedReference和AtomicMarkableReference两者的区别
- AtomicStampedReference 需要我们传入 整型变量 作为版本号,来判定是否被更改过
- AtomicMarkableReference需要我们传入布尔变量 作为标记,来判断是否被更改过
原子数组
- 保证数组内的元素的线程安全
- 使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray:引用类型数组原子类
上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray
为例子来介绍。实例代码
普通数组内元素, 多线程访问会造成安全问题
class AtomicArrayTest {
public static void main(String[] args) {
//想要获得的效果是在一个长度为10的数组里面,将其里面的每个元素从0自增到10000
//调用demo方法
demo(
() -> new int[10],
array -> array.length,
(array, index) -> array[index]++,
array -> System.out.println(Arrays.toString(array))
);
}
/**
* 参数1,提供数组、可以是线程不安全数组或线程安全数组
* 参数2,获取数组长度的方法
* 参数3,自增方法,回传 array, index
* 参数4,打印数组的方法
*/
// 函数式接口一:supplier 表示提供者 无中生有,没有参数,但是需要返回结果,如 ()->结果
// 函数式接口二:function 表示函数, 一个参数一个返回结果,如 (参数)->结果 , 还有一种是两个参数返回一个结果,如BiFunction (参数1,参数2)->结果
// 函数式接口三:consumer 表示消费者 一个参数但没结果 如 (参数)->void, 另一中两个参数,如BiConsumer (参数1,参数2)->void
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++) {
// 创建10个线程, 每个线程对数组作 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);
}
}
使用AtomicIntegerArray来创建安全数组 ```java //方式一: demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
); //方式二: demo(
()-> new AtomicIntegerArray(10),
AtomicIntegerArray::length,
AtomicIntegerArray::getAndIncrement,
System.out::println
);

<a name="ooCU1"></a>
### 字段更新器
保证多线程访问同一个对象的成员变量时, **成员变量**的线程安全性。
- AtomicReferenceFieldUpdater —引用类型的属性
- AtomicIntegerFieldUpdater —整形的属性
- AtomicLongFieldUpdater —长整形的属性
注意:利用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合 volatile 修饰的字段使用,否则会出现异常。<br />使用示例
```java
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;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
原子累加器 LongAddr极其源码 (重要)
CAS底层原理
概述
java中提供了对CAS操作的支持,具体在sun.misc.unsafe类中。这个unsafe并不是指线程的不安全,而是因为它太底层了不建议程序员直接去操作它。
- Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过
**反射**
获得(**该对象其源码被声明为单例且私有,故需要反射进行获取**) - 可以发现我们之前学习的CAS、LockSupport、
AtomicInteger
以及其他的原子类, 底层都使用的是**Unsafe**
类中的相应方法
- 其中底层的
Unsafe
有关CAS的相关方法实现原子操作的方法声明如下(当然该类的其他方法也可以操作内存) ```java /* 参数: var1:表示要操作的对象 var2:表示要操作的对象中属性地址的偏移量,我们只能知道对象的地址,对象中的属性地址得根据偏移量获取 var4:表示要修改的期望值,只有跟修改前一致才能进行修改 var5:表示要修改为的新值
这几个方法compareAndSwapXXXX,Object表示要修改对象的类型为Object,另外两个方法同理 */ public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
这里我们演示一下使用unsafe对象直接对Person对象中的属性进行线程安全的修改。因为它的底层是通过对内存的偏移量来定位到Person对象的属性的,所以比较复杂,在定位到属性后再进行比较操作;
所以我们首先要获取到属性的偏移量(偏移地址),这个可以调用unsafe对象的objectFieldOffset方法来获取;获取完偏移地址就可以对里面的属性值进行cas修改了。
```java
public class Test {
public static void main(String[] args) throws Exception {
// 通过反射获得Unsafe对象
Class unsafeClass = Unsafe.class;
// 获得构造函数,Unsafe的构造函数为私有的
Constructor constructor = unsafeClass.getDeclaredConstructor();
// 设置为允许访问私有内容
constructor.setAccessible(true);
// 创建Unsafe对象
Unsafe unsafe = (Unsafe) constructor.newInstance();
// 创建Person对象
Person person = new Person();
// 获得其属性 name 的偏移量
long nameOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("name"));
long ageOffset = unsafe.objectFieldOffset(Person.class.getDeclaredField("age"));
// 通过unsafe的CAS操作改变值
//compareAndSwapObject方法是比较并交换引用地址
unsafe.compareAndSwapObject(person, nameOffset, null, "张三");
//compareAndSwapInt方法是比较并交换int
unsafe.compareAndSwapInt(person, ageOffset, 0, 22);
System.out.println(person);
}
}
class Person {
// 配合CAS操作,必须用volatile修饰
volatile String name;
volatile int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
使用Unsafe模拟实现原子类
设计的大体思路就是先利用反射获取到一个unsafe对象,并且计算出要保护的变量属性的偏移量;然后利用unsafe对象结合cas操作完成赋值时的原子性。
class MyAtomicInteger{
//MyAtomicInteger类中要保护的是int类型的value,因为要配合cas,所以用volatile进行修饰
private volatile int value;
private static final long valueOffset;
//声明一个静态的unsafe对象
private static final Unsafe UNSAFE;
static {
UNSAFE=UnsafeAccessor.getUnsafe();
//计算要保护变量的偏移量
try {
valueOffset=UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
//要通过unsafe对象操作数据需要先得到域的偏移量,然后得到偏移量时可能抛出找不到域的异常,由于这种异常不会发生,
// 所以转化成非受查异常。在后续调用方法就不用处理这个异常。
throw new RuntimeException(e);
}
}
public int getValue(){
return value;
}
public void decrement(int amount){
//while循环保证赋值时的原子性
while (true){
int prev = this.value;
int next = prev-amount;
//利用unsafe对象完成赋值
if (UNSAFE.compareAndSwapInt(this,valueOffset,prev,next)){
break;
}
}
}
}
/*
* 获取一个unsafe对象
* */
class UnsafeAccessor{
private static final 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);
}
}
public static Unsafe getUnsafe(){
return UNSAFE;
}
}
CAS的实现原理
原理概述
CAS操作是通过调用JNI(Java本地接口)的代码实现,JNI: java Native Interface,允许java调用其它的语言。而compareAndSwapxxx系列的方法就是借助“C语言”来调用cpu底层指令实现的。
以常用的Intel x86(英特尔)平台来说,最终映射到的cpu的指令为“cmpxchg”,这是一个原子指令,涉及汇编语言,给相应指令加了lock前缀进行lock加锁保证原子,cpu执行此命令时,实现比较并替换的操作!
也就是说在现代计算机动不动就上百核心的情况下,cmpxchg是这样保证多核心下的线程安全的:
在系统底层进行CAS操作的时候,会判断当前系统是否为多核心系统,如果是就给“总线”加锁,只有一个线程会对总线加锁成功,加锁成功之后才会执行CAS操作,也就是说CAS的原子性是平台级别的 !
CAS存在的ABA问题
在之前原子类那里已经介绍过了,这里不再赘述。