提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、问题引出 - 共享资源保护问题
1.1 线程安全案例
现我们假设一个账户上的余额有10000元,使用1000个线程来模拟取款,每个线程取出的金额为10元。看看1000个线程执行完毕后账户的余额情况是否为0。
编写账户接口类Account:
public interface Account {
/**
* 获得账户余额
* @return
*/
Integer getBalance();
/**
* 取款
* @param amount 取出的金额
*/
void withDraw(Integer amount);
/**
* 模拟1000个线程取款的情况
* @param account
*/
static void demo(Account account) {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(() -> {
account.withDraw(10);
}, "取款线程-" + (i + 1));
threads.add(thread);
}
long start = System.currentTimeMillis();
threads.forEach(Thread::start);
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
}
}
创建模拟存在线程安全问题的AccountUnsafe类:
public class AccountUnsafe implements Account {
private Integer balance;
public AccountUnsafe(Integer balance) {
this.balance = balance;
}
/**
* 获得账户余额
*
* @return
*/
@Override
public Integer getBalance() {
return balance;
}
/**
* 取款
*
* @param amount 取出的金额
*/
@Override
public void withDraw(Integer amount) {
this.balance -= amount;
System.out.println(Thread.currentThread().getName() + "取款10元...");
}
}
编写测试类如下:
public class UnsafeTest {
public static void main(String[] args) {
Account account = new AccountUnsafe(10000);
Account.demo(account);
}
}
运行结果如下图所示(每次运行得到的结果可能都不一样):
从上面结果可以看出,多个线程对于账户中的共享变量balance
的读写交错执行
引发了线程安全问题,导致最终结果并不是期望的0。
1.2 synchronized解决线程安全问题
现在我们使用加锁
的方式即synchronized
关键字来解决上面取款案例的线程安全问题,对于共享变量balance的读操作和写操作进行一个加锁保护。编写新的账户类AccountSyncSafe如下:
public class AccountSyncSafe implements Account {
private Integer balance;
/**
* 获得账户余额
*
* @return
*/
@Override
public Integer getBalance() {
synchronized (this) {
return balance;
}
}
/**
* 取款
*
* @param amount 取出的金额
*/
@Override
public void withDraw(Integer amount) {
synchronized (this) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取款10元...");
}
}
}
编写新的测试类如下:
public class SyncSafeTest {
public static void main(String[] args) {
Account account = new AccountSyncSafe(10000);
Account.demo(account);
}
}
运行结果如下所示:
可以看到账户余额可以正确地显示为0,即成功解决了共享资源的线程安全问题。sychronized关键字是使用了加锁的方式来解决线程安全问题的,那么有没有不使用锁也能解决线程安全问题呢?这就要使用到Java并发编程中的CAS知识。
1.3 CAS解决线程安全问题
现在我们使用CAS来解决上述取款案例中的线程安全问题,CAS中提供了各种原子类
(后续文章内容会介绍),本次我们使用的是原子整数类AtomicInteger。编写新的账户类如下:
public class AccountAtomicSafe implements Account {
private AtomicInteger balance;
public AccountAtomicSafe(Integer balance) {
this.balance = new AtomicInteger(balance);
}
/**
* 获得账户余额
*
* @return
*/
@Override
public Integer getBalance() {
return balance.get();
}
/**
* 取款
*
* @param amount 取出的金额
*/
@Override
public void withDraw(Integer amount) {
while (true) {
//当前余额
int prev = balance.get();
//取款后需要设置的余额
int next = prev - amount;
//执行修改--修改成功则退出
if (balance.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + ": 取款10元...");
break;
}
}
}
}
编写测试类如下:
可以看到,使用无锁的CAS方式也能解决共享变量的线程安全问题。
二、CAS
2.1 概念
Java中的CAS(Compared-And-Set或Compare-And-Swap),是一种无锁同步
技术,常用于原子操作(CPU指令级别的原子操作,不可分割),其本质就是一个原子指令,例如,在X86架构中,CAS底层对应的是lock cmpxchg
指令,在单核CPU和多核CPU下都能保证【比较-交换】操作的原子性。
CAS包含三个操作数:
- 内存位置(
V
)- 表示需要进行操作的变量的位置。 - 预期值(
A
)- 表示在该内存位置的预期值。 - 新值(
B
)- 表示需要写入该内存位置的新值。
CAS操作执行步骤如下:
- 比较内存位置
V
处的值与预期值A
是否相等。 - 如果相等,则将内存位置
V
处的值更新为B
。 - 如果不相等,则不做任何操作。
整个执行过程不可被中断
,在多线程情况下,只有一个线程可以成功更新值,其他线程失败并重新尝试。
2.2 案例回顾
2.2.1 分析
在1.3章节我们使用CAS对取款案例进行了一个改造,其中取款并更新余额的代码如下:
public void withDraw(Integer amount) {
while (true) {
//当前余额
int prev = balance.get();
//取款后需要设置的余额
int next = prev - amount;
//执行修改--修改成功则退出
if (balance.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + ": 取款10元...");
break;
}
}
}
整个逻辑包含在一个while循环中,如果当前线程更新余额成功,则退出该循环,否则需要不断地失败重试,更新余额时的核心逻辑compareAndSet
。该过程简要示意图可如下:
可以看出,CAS有两个核心要素
:比较-设值、失败重试。
2.2.2 CAS和valotile
回顾代码逻辑:
public void withDraw(Integer amount) {
while (true) {
//当前余额
int prev = balance.get();
//取款后需要设置的余额
int next = prev - amount;
//执行修改--修改成功则退出
if (balance.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + ": 取款10元...");
break;
}
}
}
在方法withDraw中,prev
和next
是两个局部变量,存储在每个线程的工作内存
中,balance
是共享变量,存储在主存
中【主存是所有线程共享的内存区域,用于存储程序中所有的变量(包括实例变量和静态变量)】。线程工作内存区域是线程私有的内存区域,对变量的读写要在工作内存中完成,并不能直接操作主存,且线程操作的是变量的工作内存副本,所以在多线程环境下一个线程对共享变量的修改对于其他线程不可见。那么CAS又是如何确保了共享变量的修改对所有线程可见呢?
我们可以看看案例中使用的原子整数类ActomicInteger类的源码:
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
...
可以看到,对于成员变量value使用了volatile
修饰,即可以把value的最新值更新到主存,避免了线程从自己的工作缓存中查找变量的值,当线程需要操作共享变量时,都是到主存中获取对应的值,这就使得一个线程对共享变量的修改,对于其他线程是可见的。CAS只有借助volatile 才能读取共享变量的最新值来实现比较-交换
。
三、CAS提供的原子类
Java提供的原子类在java.util.concurrent.atomic包下:
3.1 原子整数
我们可以使用原子整数AtomicInteger来看一下一些常用API。
3.1.1 基础运算
public class AtomicIntegerDemo {
public static void main(String[] args) {
AtomicInteger num = new AtomicInteger(0);
//先自增 再获取值 => 1
int num1 = num.incrementAndGet();
System.out.println("num1: " + num1);
//先获取值 再自增 => 输出1 值变为2
int num2 = num.getAndIncrement();
System.out.println("num2: " + num2);
//按指定大小做增减 => 输出2 值变为12
int num3 = num.getAndAdd(10);
System.out.println("num3: " + num3);
//22
int num4 = num.addAndGet(10);
System.out.println("num4: " + num4);
}
}
回顾上述线程安全问题案例:
public void withDraw(Integer amount) {
while (true) {
//当前余额
int prev = balance.get();
//取款后需要设置的余额
int next = prev - amount;
//执行修改--修改成功则退出
if (balance.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + ": 取款10元...");
break;
}
}
}
现在可以直接调用API
来替换代码逻辑:
public void withDraw(Integer amount) {
balance.getAndAdd(-1 * amount);
}
运行查看结果发现最终余额显示正常:
3.1.2 复杂运算
对一些稍微复杂的运算,可以调用API如下:
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
@FunctionalInterface
public interface IntUnaryOperator {
...
IntUnaryOperator 是一个函数式接口
。现假设有一个原子整数变量,若该数为奇数,则该数的值翻一倍,反之若该数为偶数,则该数的值变为原值的1/2。实现示例代码如下:
public class AtomicDemo {
static int initValue = 10;
public static void main(String[] args) {
AtomicInteger num = new AtomicInteger(initValue);
num.updateAndGet(value -> {
if (value % 2 == 0) {
value = value / 2;
} else {
value = value * 2;
}
return value;
});
System.out.println("num: " + num.get());
}
}
10是偶数,所以输出结果为5。
测试奇数如下:
public class AtomicDemo {
static int initValue = 13;
public static void main(String[] args) {
AtomicInteger num = new AtomicInteger(initValue);
num.updateAndGet(value -> {
if (value % 2 == 0) {
value = value / 2;
} else {
value = value * 2;
}
return value;
});
System.out.println("num: " + num.get());
}
}
输出结果为26:
3.2 原子引用
Java还提供了一些原子引用类:
要保护的数据类型不仅仅只是基本数据类型
,当需要保护引用数据类型
的时候,就需要使用到相关原子引用类。
3.2.1 基本使用
现在使用原子引用类AtomicReference
对上述的线程安全案例做一个改造。
public interface Account {
/**
* 获得账户余额
* @return
*/
BigDecimal getBalance();
/**
* 取款
* @param amount 取出的金额
*/
void withDraw(BigDecimal amount);
/**
* 模拟1000个线程取款的情况
* @param account
*/
static void demo(Account account) {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(() -> {
account.withDraw(BigDecimal.TEN);
}, "取款线程-" + (i + 1));
threads.add(thread);
}
long start = System.currentTimeMillis();
threads.forEach(Thread::start);
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
}
}
public class AccountReferSafe implements Account {
private AtomicReference<BigDecimal> balance;
public AccountReferSafe(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 = balance.get().subtract(amount);
if (balance.compareAndSet(prev, next)) {
System.out.println(Thread.currentThread().getName() + ": 取款10元...");
break;
}
}
}
}
编写测试类如下:
public class AtomicReferTest {
public static void main(String[] args) {
Account account = new AccountReferSafe(new BigDecimal(10000));
Account.demo(account);
}
}
查看测试结果依旧不会出现线程安全问题:
3.2.2 ABA问题
在原子类的使用中,有一个非常经典的问题即ABA问题
。ABA问题是指在多线程环境下,一个线程在检查一个变量的值,并决定是否更新这个变量时,该变量的值在此期间被其他线程修改了,但最后又改回原来的值。尽管变量的值看起来没有变化,实际上它经历了改变和恢复
的过程。这种情况会导致逻辑上的错误。例如:原子变量num初始值为1
,线程T1读取变量num的值,发现是1
,然后准备将其值改为2
。在这期间,线程T2也在操作变量A,先将其值改为3
,然后又将其改回1
。当线程T1再次检查变量A的值时,它仍然是1
,线程T1错误地认为变量的值没有被其他线程修改过,于是将变量A的值从1
改为2
。
3.2.2.1 案例
编写ABA问题案例如下:
public class AtomicABADemo {
static AtomicReference<String> var = new AtomicReference<>("A");
public static void main(String[] args) {
try {
log.debug(" main线程...");
//获取变量值
String prev = var.get();
log.debug("var: {}", prev);
//模拟其他线程的修改操作
otherThreadUpdate();
TimeUnit.SECONDS.sleep(2);
//将A修改为C
log.debug("main: A -> C => {}", var.compareAndSet(prev, "C"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void otherThreadUpdate() {
new Thread(() -> {
log.debug("other1: A -> B => {}", var.compareAndSet(var.get(), "B"));
}, "other1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
log.debug("other2: B -> A => {}", var.compareAndSet(var.get(), "A"));
}, "other2").start();
}
}
运行结果如下:
主线程main感知不到其他线程已经对变量var做了修改。
3.2.2.2 解决方案1 - AtomicStampedReference
ABA问题的解决方案是:加上版本号
来表示每次的更新操作。例如,每次更新变量时,不仅更新变量的值,还更新其版本号。这样,即使变量的值看起来没有变化,但版本号的变化可以帮助检测到实际的变化。
Java提供了AtomicStampedReference
来处理ABA问题,它将版本号(stamp
)与引用(reference
)捆绑在一起,以便检测ABA问题。
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
private volatile Pair<V> pair;
...
使用AtomicStampedReference对上述的ABA问题案例改造如下:
public class ABASolveDemo {
/**
* 初始变量值:A; 版本号: 0
*/
static AtomicStampedReference<String> var = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) {
try {
log.debug(" main线程...");
//获取变量值
String prev = var.getReference();
//获取版本号
int stamp = var.getStamp();
log.debug("变量值: {}, 版本号: {}", prev, stamp);
//模拟其他线程的修改操作
otherThreadUpdate();
TimeUnit.SECONDS.sleep(2);
//将A修改为C
log.debug("当前版本号: {}", var.getStamp());
log.debug("main: A -> C => {}", var.compareAndSet(prev, "C", stamp, stamp + 1));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void otherThreadUpdate() {
new Thread(() -> {
int stamp = var.getStamp();
log.debug("当前版本号: {}", stamp);
//更新成功: 版本号 + 1
log.debug("other1: A -> B => {}", var.compareAndSet(var.getReference(), "B",
stamp, stamp + 1));
}, "other1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
int stamp = var.getStamp();
log.debug("当前版本号: {}", stamp);
log.debug("other2: B -> A => {}", var.compareAndSet(var.getReference(), "A",
stamp, stamp + 1));
}, "other2").start();
}
}
运行结果如下:
main线程并不能将变量值从A
修改为C
,因为其他线程对变量做了修改,版本号已经是2而不再是0。
3.2.2.3 解决方案2 - AtomicMarkableReference
AtomicStampedReference通过一个整数标记-版本号
来追踪原子引用的整个过程,每次引用更新,版本号也跟着更新。但是如果我们并不想过于关注引用值变化了几次,只想关注引用是否更改过
,这是就需要使用AtomicMarkableReference
。
public class AtomicMarkableReference<V> {
private static class Pair<T> {
final T reference;
final boolean mark;
private Pair(T reference, boolean mark) {
this.reference = reference;
this.mark = mark;
}
static <T> Pair<T> of(T reference, boolean mark) {
return new Pair<T>(reference, mark);
}
}
private volatile Pair<V> pair;
...
AtomicMarkableReference维护了一个引用和一个布尔标记(mark
),并通过检查和设置这些值来确保原子操作的正确性。
假设一个场景:在一栋房屋中,垃圾袋(garbagebag
)满了以后,如果保洁员(housekeeper
)有空那么保洁员就换上一个新垃圾袋,如果主人(main
)有空那么主人就换上一个新垃圾袋。如果已经换上了新垃圾袋,那么就不需要再换上新垃圾袋。
编写垃圾袋类如下:
public class GarbageBag {
private String desc;
public GarbageBag(String desc) {
this.desc = desc;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "GarbageBag{" +
"desc='" + desc + '\'' +
'}';
}
}
测试类如下[假设初始场景垃圾袋就已经满了
]:
public class ABASolveDemo2 {
public static void main(String[] args) {
GarbageBag bag = new GarbageBag("装满了垃圾");
AtomicMarkableReference<GarbageBag> ref = new AtomicMarkableReference<>(bag, true);
log.debug("main...");
GarbageBag prev = ref.getReference();
log.debug("垃圾袋: {}", prev.toString());
//保洁员
new Thread(() -> {
log.debug("保洁员...");
//垃圾袋已满,需要换上新垃圾袋
bag.setDesc("新垃圾袋");
if (ref.compareAndSet(bag, bag, true, false)) {
log.debug("换了一只新垃圾袋...");
}
}, "housekeeper").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("需要换一只新垃圾袋吗?");
boolean isSuccess = ref.compareAndSet(bag, new GarbageBag("新垃圾袋"), true, false);
log.debug("是否换了新垃圾袋: {}", isSuccess);
log.debug("垃圾袋: {}", bag.toString());
}
}
运行结果:
3.3 原子数组
3.3.1 问题演示
现模拟一个多线程下普通数组存在线程安全场景。假设有一个长度为10的整数数组,初始值都是0,现有10个[数组长度
]线程对数组的每个元素操作10000次自增加1
操作。代码如下:
public class ArrayUnSafe {
/**
* 数组测试案例
* @param arraySupplier 提供数组
* @param lengthFun 获得数组长度
* @param putConsumer 自增方法 => 返回值(array index)
* @param printConsumer 打印数组
* @param <T>
*/
public static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer
) {
List<Thread> threads = new ArrayList<>();
T array = arraySupplier.get();
Integer arrLength = lengthFun.apply(array);
for (int i = 0; i < arrLength; i++) {
//每个线程对每个元素操作10000次:自增+1
threads.add(
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
//回传array index => 获得数组对应下标元素
putConsumer.accept(array, j % arrLength);
}
}, "t" + (i + 1))
);
}
//启动所有线程
threads.forEach(Thread::start);
//等待所有线程执行完毕
threads.forEach(thread -> {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//打印查看数组
printConsumer.accept(array);
}
}
测试类如下:
public class ArrayUnSafeTest {
public static void main(String[] args) {
ArrayUnSafe.demo(
() -> new int[10],
(array) -> array.length,
(array, index) -> array[index]++,
array -> System.out.println(Arrays.toString(array))
);
}
}
查看运行结果:
多次运行,发现结果并不是想象中的[10000,10000,10000,10000,10000,10000,10000,10000,10000,10000]
。
3.3.2 问题解决
在上面数组元素累加案例中,由于多线程下读写交错执行
导致出现线程安全问题,最终输出结果和预期结果并不相同。现在使用原子数组AtomicIntegerArray来改造上述案例,如下:
public class ArraySafeTest {
public static void main(String[] args) {
System.out.println("普通数组...");
ArrayUnSafe.demo(
() -> new int[10],
(array) -> array.length,
(array, index) -> array[index]++,
array -> System.out.println(Arrays.toString(array))
);
System.out.println("原子数组...");
ArrayUnSafe.demo(
() -> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
}
}
输出结果如下:
可以看到,使用原子数组解决了线程安全问题。
3.4 原子更新器
Java还提供了字段更新器
来专门针对对象的某个域进行原子操作,需要配合valotile
修饰,且访问控制符不能是private
,否则将会抛出异常。示例如下:
public class Worker {
volatile String name;
@Override
public String toString() {
return "Worker{" +
"name='" + name + '\'' +
'}';
}
}
测试类如下(假设当前只有主线程在执行):
public class CasUpdateDemo {
public static void main(String[] args) {
Worker worker = new Worker();
AtomicReferenceFieldUpdater<Worker, String> ref = AtomicReferenceFieldUpdater
.newUpdater(Worker.class, String.class, "name");
//如果当前为null 则修改为qmc
log.debug("修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
System.out.println(worker);
}
}
运行结果:
现假设还有其他线程同样尝试修改name属性值:
public class CasUpdateDemo {
static Random random = new Random();
public static void main(String[] args) {
Worker worker = new Worker();
AtomicReferenceFieldUpdater<Worker, String> ref = AtomicReferenceFieldUpdater
.newUpdater(Worker.class, String.class, "name");
new Thread(() -> {
try {
//随机休眠
int time = random.nextInt(3) + 1;
log.debug("休眠时间: {}", time);
TimeUnit.SECONDS.sleep(time);
log.debug("是否修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t1").start();
//如果当前为null 则修改为qmc
//随机休眠
try {
int time = random.nextInt(3) + 1;
log.debug("休眠时间: {}", time);
TimeUnit.SECONDS.sleep(time);
log.debug("是否修改成功: {}", ref.compareAndSet(worker, null, "qmc"));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(worker);
}
}
运行结果如下:
如果其他线程已经修改了对应属性值,则当前线程执行修改会失败。
3.5 原子累加器
Java还提供了原子累加器来专门做累加计算
,其性能比单独使用原子整数等做累加的效率要高。示例代码如下:
public class CasAdderDemo {
public static void main(String[] args) {
log.info("普通原子长整型做累加计算...");
for (int i = 0; i < 5; i++) {
demo(
() -> new AtomicLong(0),
(adder) -> adder.getAndIncrement()
);
}
log.info("原子累加器做累加计算...");
for (int i = 0; i < 5; i++) {
demo(
() -> new LongAdder(),
(adder) -> adder.increment()
);
}
}
/**
* 测试样例方法
* @param adderSupplier 累加计算的原子类或者原子累加器
* @param action 累加操作
* @param <T>
*/
private static <T> void demo(
Supplier<T> adderSupplier,
Consumer<T> action
) {
T adder = adderSupplier.get();
List<Thread> threads = new ArrayList<>();
//4个线程:每个线程累加500000次
for (int i = 0; i < 4; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j < 500000; j++) {
action.accept(adder);
}
}, "adder" + (i + 1)));
}
long start = System.currentTimeMillis();
//启动
threads.forEach(Thread::start);
//等待执行完毕
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
log.debug("本次累加计算耗时: {}ms", (end - start));
}
}
运行结果如下所示:
可以看到,使用累加器计算的速度能比单独使用原子类计算大概快几倍左右。
四、UnSafe对象
4.1 概念
在Java中,UnSafe类是一个比较偏底层
的类,一般来说较少会直接操作这个类的对象(光是看名字都有点吓人了
),UnSafe对象提供了非常底层的,操作内存、线程的方法,允许开发者直接操作内存和线程,从而绕过Java虚拟机的一些安全机制,如果使用不当出现错误可能导致程序崩溃、数据损坏甚至安全漏洞。
4.2 获取
Unsafe类是JDK内部使用的,无法通过常规的方式直接获取,只能通过反射
的方式来获取,大致看一下UnSafe源码如下:
public final class Unsafe {
private static final Unsafe theUnsafe;
public static final int INVALID_FIELD_OFFSET = -1;
public static final int ARRAY_BOOLEAN_BASE_OFFSET;
...
theUnsafe私有且是单例。可以通过反射获取Filed来获取UnSafe对象,代码如下:
public class GetUnSafeDemo {
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);
}
}
运行结果:
4.3 使用示例
4.3.1 CAS方式操作对象属性
public class Student {
volatile int id;
volatile String name;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}
通过UnSafe对象给Student对象属性赋值如下:
public class UnsafeUseDemo1 {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//反射爆破
theUnsafe.setAccessible(true);
//获取
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//获得属性域偏移地址
long idOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
long nameOffset = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));
Student student = new Student();
//cas操作 => param: 实例对象 属性域偏移值 原始值 修改值
unsafe.compareAndSwapInt(student, idOffset, 0, 18);
unsafe.compareAndSwapObject(student, nameOffset, null, "qmc");
System.out.println(student);
}
}
4.3.2 直接内存操作
public class UnsafeUseDemo2 {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//反射爆破
theUnsafe.setAccessible(true);
//获取
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
//分配1kb的内存
long memory = unsafe.allocateMemory(1024);
//初始化内存
unsafe.setMemory(memory, 1024, (byte) 0);
//释放内存
unsafe.freeMemory(memory);
}
}
4.3.3 数组操作
public class UnsafeUseDemo3 {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
//反射爆破
theUnsafe.setAccessible(true);
//获取
Unsafe unsafe = (Unsafe) theUnsafe.get(null);
int[] array = new int[10];
//数组基地址
int baseOffset = unsafe.arrayBaseOffset(int[].class);
//元素偏移量
int indexScale = unsafe.arrayIndexScale(int[].class);
//设置第6个元素为42
unsafe.putInt(array, baseOffset + indexScale * 5, 42);
//获取第6个元素的值
int sixVal = unsafe.getInt(array, baseOffset + indexScale * 5);
System.out.println("第6个元素的值: " + sixVal);
}
}
4.4.4 模拟实现原子整数
使用UnSafe对象对本文开头的取款案例进行一个改造。定义一个获取UnSafe对象的类MyUnSafeAccessor :
public class MyUnSafeAccessor {
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) {
//转为运行时异常抛出
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static Unsafe getUnsafe() {
return unsafe;
}
}
账户接口:
public interface Account {
/**
* 获得账户余额
* @return
*/
Integer getBalance();
/**
* 取款
* @param amount 取出的金额
*/
void withDraw(Integer amount);
/**
* 模拟1000个线程取款的情况
* @param account
*/
static void demo(com.zkt.article.demo1.Account account) {
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(() -> {
account.withDraw(10);
}, "取款线程-" + (i + 1));
threads.add(thread);
}
long start = System.currentTimeMillis();
threads.forEach(Thread::start);
threads.forEach(t -> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
long end = System.currentTimeMillis();
System.out.println("账户余额: " + account.getBalance() + ", 执行时间: " + (end - start) / 1000);
}
}
账户实现类:
public class MyAccount implements Account {
/**
* Unsafe对象
*/
private static final Unsafe unsafe;
/**
* 属性偏移量
*/
private static final long valueOffset;
/**
* 属性值 -- volatile 保证可见性
*/
private volatile int balance;
static {
unsafe = MyUnSafeAccessor.getUnsafe();
try {
valueOffset = unsafe.objectFieldOffset(MyAccount.class.getDeclaredField("balance"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public MyAccount(int balance) {
this.balance = balance;
}
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withDraw(Integer amount) {
while (true) {
int prev = balance;
int next = balance - amount;
if (unsafe.compareAndSwapInt(this, valueOffset, prev, next)) {
log.info("取款: {}元成功", amount);
break;
}
}
}
}
编写测试类如下:
public class MyTest {
public static void main(String[] args) {
Account account = new MyAccount(10000);
Account.demo(account);
}
}
测试结果如下:
可以看到,通过UnSafe对象实现的CAS操作也能有效防止线程安全问题。
总结
CAS可以实现无锁并发
,适合于线程数较少、多核CPU的场景下。CAS基于乐观锁
的设计思想,即最乐观地估计不会发生线程安全问题,等出现线程安全问题时执行重试操作。CAS体现的是无锁并发
、无阻塞并发
,因为没有使用synchronized,所以线程并不会进入阻塞状态。但是如果线程之间的竞争十分激烈,则重试必然要频繁地发生,那么效率就会收到影响。本次关于CAS部分的知识就分享到这了。