一、问题引出
1.1 需求与问题
需求:模拟一千个线程进行取款操作,需要保证取款方法的线程安全。
- 代码示例(线程不安全):
public interface Account {
/**
* 获取余额。
*
* @return {@link Integer}
*/
Integer getBalance();
/**
* 取款。
*
* @param amount 数额
*/
void withdraw(Integer amount);
/**
* 模拟一千个用户取款的方法:
* 方法内会启动 1000 个线程,每个线程做 -1 元 的操作;
* 如果初始余额为 1000 那么正确的结果应当是 0。
*
* @param account 账户
* @throws InterruptedException 中断异常
*/
static void oneThousandUsersWithdraw(Account account) throws InterruptedException {
List<Thread> threads = new ArrayList<>();
Instant start = Instant.now();
for (int i = 0; i < 1000; i++) {
threads.add(new Thread(() -> account.withdraw(1)));
}
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
Instant end = Instant.now();
long costTime = Duration.between(start, end).toMillis();
System.out.println("余额为:" + account.getBalance() + ",花费时间为:" + costTime + "ms。");
}
@AllArgsConstructor
class AccountUnsafe implements Account {
private Integer balance;
@Override
public Integer getBalance() {
return balance;
}
@Override
public void withdraw(Integer amount) {
balance -= amount;
}
public static void main(String[] args) throws InterruptedException {
Account.oneThousandUsersWithdraw(new AccountUnsafe(1000));
// 第一次执行结果:余额为:7,花费时间为:86ms。
// 第二次执行结果:余额为:3,花费时间为:89ms。
}
}
}
1.2 解决方式
- 使用『锁』方式解决线程不安全问题:
@AllArgsConstructor
class AccountSafeByLock implements Account {
private Integer balance;
@Override
public synchronized Integer getBalance() {
return balance;
}
@Override
public synchronized void withdraw(Integer amount) {
balance -= amount;
}
public static void main(String[] args) throws InterruptedException {
Account.oneThousandUsersWithdraw(new AccountSafeByLock(1000));
// 第一次执行结果:余额为:0,花费时间为:92ms。
// 第二次执行结果:余额为:0,花费时间为:95ms。
}
}
- 使用『无锁』方式解决线程不安全问题:
class AccountSafeByNoLock implements Account {
private final AtomicInteger balance;
public AccountSafeByNoLock(Integer balance) {
this.balance = new AtomicInteger(balance);
}
@Override
public Integer getBalance() {
return this.balance.get();
}
@Override
public void withdraw(Integer amount) {
while (true) {
int prev = getBalance();
int next = prev - amount;
if (this.balance.compareAndSet(prev, next)) {
break;
}
}
}
public static void main(String[] args) throws InterruptedException {
Account.oneThousandUsersWithdraw(new AccountSafeByNoLock(1000));
// 第一次执行结果:余额为:0,花费时间为:84ms。
// 第二次执行结果:余额为:0,花费时间为:85ms。
}
}
二、CAS 与 volatile
2.1 CAS
上述使用到的
AtomicInteger
的解决方法,内部并没有用锁来保护共享变量的线程安全。那么它是如何实现的呢?
- 代码说明:
@Override
public void withdraw(Integer amount) {
while (true) {
// 1.假设此处取到值为 1000。
int prev = getBalance();
// 2.在 1000 基础上减 1 = 999。
int next = prev - amount;
/* *
* 3.compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值:
* 3.1.不一致了,next 作废,返回 false 表示失败。
* 比如,别的线程已经做了减法,当前值已经被减成了 999,那么本线程的这次 999 就作废了,进入 while 下次循环重试。
*
* 3.2.一致,以 next 设置为新值,返回 true 表示成功,并结束循环。
*/
if (this.balance.compareAndSet(prev, next)) {
break;
}
}
}
- 其中的关键是
compareAndSet()
,它的简称就是CAS
(也有 Compare And Swap 的说法),它必须是原子操作。 - 示意图:
- 注意:
- 其实
CAS
的底层是lock cmpxchg
指令(X86
架构),在单核cpu
和多核cpu
下都能够保证『比较-交换』的原子性。 - 在多核状态下,某个核执行到带
lock
的指令时,cpu
会让总线锁住,当这个核把此指令执行完毕,再开启总线。这个过程中不会被线程的调度机制所打断,保证了多个线程对内存操作的准确性,是原子的。
- 其实
2.2 volatile
- 获取共享变量时,为了保证该变量的可见性,需要使用
volatile
修饰。 - 它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作
volatile
变量都是直接操作主存。即一个线程对volatile
变量的修改,对另一个线程可见。 CAS
必须借助volatile
才能读取到共享变量的最新值来实现『比较-交换』的效果。
2.3 为什么无锁效率高?
- 无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而
synchronized
会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。 - 打个比喻:线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速... 恢复到高速运行,代价比较大。
- 但无锁情况下,因为线程要保持运行,需要额外
cpu
的支持,cpu
在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
2.4 CAS 的特点
- 结合
CAS
和volatile
可以实现无锁并发,适用于线程数少、多核cpu
的场景下。 CAS
是基于乐观锁的思想:不怕别的线程来修改共享变量,就算改了也没关系,大不了再进行重试。synchronized
是基于悲观锁的思想:得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。CAS
体现的是无锁并发、无阻塞并发:- 因为没有使用
synchronized
,所以线程不会陷入阻塞,这是效率提升的因素之一。 - 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。
- 因为没有使用
三、原子整数
java.util.concurrent
并发包提供了:AtomicBoolean
AtomicInteger
AtomicLong
- 此处以
AtomicInteger
为例:
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)
// 其中函数中的操作能保证原子,但函数需要无副作用
// 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))
四、原子引用
4.1 AtomicReference
一个可以被原子化更新的对象引用,多线程操作下,不会使
AtomicReference
最终达到不一致的状态。
- 代码示例:
@Slf4j
public class AtomicReferenceSample {
private static AtomicReference<String> ref = new AtomicReference<>("initValue");
private static void otherThreadCAS() throws InterruptedException {
new Thread(() -> {
boolean isSuccess = ref.compareAndSet(ref.get(), "value 1");
log.debug("t1 try set={}", isSuccess);
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
boolean isSuccess = ref.compareAndSet(ref.get(), "value 2");
log.debug("t2 try set={}", isSuccess);
}, "t2").start();
}
public static void main(String[] args) throws InterruptedException {
otherThreadCAS();
TimeUnit.SECONDS.sleep(1);
boolean isSuccess = ref.compareAndSet(ref.get(), "value 3");
log.debug("main try set={}", isSuccess);
log.debug("last value={}", ref.get());
// [t1] t1 try set=true
// [t2] t2 try set=true
// [main] main try set=true
// last value=value 3
}
}
- 问题点:主线程仅能判断出共享变量的值与最初值是否相同,不能感知到值改变后又改回的中间情况。
- 只要有其它线程动过了共享变量,那么自己的
cas
就算失败,这时,仅比较值是不够的,需要再加一个标记,此时推荐使用AtomicStampedReference
。
4.2 AtomicStampedReference
一个带标记的原子化更新对象引用。
- 代码示例:
@Slf4j
public class AtomicStampedReferenceSample {
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("initValue", 0);
private static void otherThreadCAS() throws InterruptedException {
new Thread(() -> {
log.debug("t1 try set={}", ref.compareAndSet(ref.getReference(), "value 1",
ref.getStamp(), ref.getStamp() + 1));
log.debug("t1 stamp={}", ref.getStamp());
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
log.debug("t2 try set={}", ref.compareAndSet(ref.getReference(), "initValue",
ref.getStamp(), ref.getStamp() + 1));
log.debug("t2 stamp={}", ref.getStamp());
}, "t2").start();
}
public static void main(String[] args) throws InterruptedException {
// 获取值。
String prev = ref.getReference();
// 获取版本号。
int stamp = ref.getStamp();
log.debug("main stamp={}", stamp);
// 模拟中间有其它线程干扰。
otherThreadCAS();
TimeUnit.SECONDS.sleep(1);
// 尝试改为 value 3
log.debug("main try set={}", ref.compareAndSet(prev, "value 3", stamp, stamp + 1));
// [main] main stamp=0
// [t1] t1 try set=true
// [t1] t1 stamp=1
// [t2] t2 try set=true
// [t2] t2 stamp=2
// [main] main try set=false
}
}
AtomicStampedReference
可以给原子引用加上版本号,追踪原子引用整个的变化过程。但有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,此时就推荐使用AtomicMarkableReference
。
4.3 AtomicMarkableReference
维护一个对象引用以及一个整数 "
stamp
",它可以被原子化地更新。
- 代码示例:
@Slf4j
public class AtomicMarkableReferenceSample {
private static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("initValue", true);
public static void main(String[] args) throws InterruptedException {
// 主线程获取值。
String prev = ref.getReference();
log.debug("main get ref={}", prev);
// 其他线程改变了引用变量。
new Thread(() -> {
ref.set("value 1", false);
log.debug("t1 set value");
}, "t1").start();
TimeUnit.SECONDS.sleep(1);
// 主线程尝试设置值。
boolean isSuccess = ref.compareAndSet(prev, "value 2", true, false);
log.debug("main try set={}", isSuccess);
// [main] main get ref=initValue
// [t1] t1 set value
// [main] main try set=false
}
}
五、原子数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
- 此处以
AtomicIntegerArray
为例:
@Slf4j
public class AtomicIntegerArraySample {
/**
* 示例一:使用不安全的整型数组。
*
* @throws InterruptedException 中断异常
*/
@Test
public void unSafeArray() throws InterruptedException {
// 创建一个长度为 2 的数组。
Integer[] ints = new Integer[2];
List<Thread> threads = new ArrayList<>();
// 3个线程对它执行操作。
for (int i = 0; i < 3; i++) {
// 每个线程对数组做1次操作 。
int finalI = i;
threads.add(new Thread(() -> {
for (int j = 0; j <= 1; j++) {
ints[finalI] = j;
}
}, "t_" + i));
}
// 启动线程,并等待所有线程执行结束。
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
log.debug("array={}", Arrays.toString(ints));
// Exception in thread "t_2" java.lang.ArrayIndexOutOfBoundsException
// [main] array=[1, 1]
}
/**
* 示例二:使用原子的整型数组。
*
* @throws InterruptedException 中断异常
*/
@Test
public void safeArray() throws InterruptedException {
// 创建一个长度为 2 的『原子数组』。
AtomicIntegerArray ints = new AtomicIntegerArray(2);
List<Thread> threads = new ArrayList<>();
for (int i = 0; i < 3; i++) {
threads.add(new Thread(() -> {
for (int j = 0; j <= 1; j++) {
ints.getAndIncrement(j);
}
}, "t_" + i));
}
threads.forEach(Thread::start);
for (Thread thread : threads) {
thread.join();
}
log.debug("array={}", ints);
// [main] array=[3, 3]
}
}
六、字段更新器
AtomicReferenceFieldUpdater
// 域 字段AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
- 注意:使用字段更新器,可以针对对象的某个域(Field)进行原子操作,只能配合
volatile
修饰的字段使用,否则会出现异常。 - 此处以
AtomicIntegerFieldUpdater
为例:
@Slf4j
public class AtomicIntegerFieldUpdaterSample {
/* *
* 注意:要使用字段更新器必须使用 volatile 进行修饰。
*/
private volatile int field = 0;
public static void main(String[] args) {
AtomicIntegerFieldUpdater<AtomicIntegerFieldUpdaterSample> aifu =
AtomicIntegerFieldUpdater.newUpdater(AtomicIntegerFieldUpdaterSample.class, "field");
AtomicIntegerFieldUpdaterSample sample = new AtomicIntegerFieldUpdaterSample();
// 将 field 字段值更新为 1。
aifu.compareAndSet(sample, 0, 1);
log.debug("updated field ={}", sample.field);
// updated field =1
// 将 field 字段值更新为 2。
aifu.compareAndSet(sample, 1, 2);
log.debug("updated field ={}", sample.field);
// updated field =2
// 期望值不存在,则修改失败。
aifu.compareAndSet(sample, 1, 3);
log.debug("updated field ={}", sample.field);
// updated field =2
}
}
七、原子累加器性能比较
比较
AtomicLong
与LongAdder
性能差异。
- 代码示例:
@Slf4j
public class AtomicLongAndLongAdderComparePerfSample {
/**
* 用于累加器比较性能的方法。
*
* @param sup 供给型接口 - 获取累加器。
* @param action 消费型接口 - 接收值。
* @throws InterruptedException 中断异常
*/
private static <T> void comparePerf(Supplier<T> sup, Consumer<T> action) throws InterruptedException {
T adder = sup.get();
Instant start = Instant.now();
List<Thread> ts = new ArrayList<>();
// 40 个线程,每人累加 50 万
for (int i = 0; i < 40; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 50_0000; j++) {
action.accept(adder);
}
}));
}
ts.forEach(Thread::start);
for (Thread t : ts) {
t.join();
}
Instant end = Instant.now();
long cost = Duration.between(start, end).toMillis();
log.debug("total={},cost={}ms", adder, cost);
}
public static void main(String[] args) throws InterruptedException {
// 两种累加器,各输出3次的结果。
for (int i = 0; i < 3; i++) {
comparePerf(LongAdder::new, LongAdder::increment);
}
// total=20000000,cost=79ms
// total=20000000,cost=10ms
// total=20000000,cost=9ms
log.debug("------------------------------------------------");
for (int i = 0; i < 3; i++) {
comparePerf(AtomicLong::new, AtomicLong::getAndIncrement);
}
// total=20000000,cost=271ms
// total=20000000,cost=276ms
// total=20000000,cost=202ms
}
}
- 结论:
LongAdder
性能优于AtomicLong
! LongAdder
实现思路:性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]... 最后将结果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 cas 重试失败,从而提高性能。
八、Unsafe
Unsafe
对象提供了非常底层的,操作内存、线程的方法,Unsafe
对象不能直接调用,只能通过反射获得。
8.1 CAS 操作
-
注意事项(这里 Unsafe 名字的 “不安全” 并不是指"线程不安全",而是指操作底层的不安全性,具体原因如下):
- 不受
JVM
管理,也就意味着无法被GC
,需要我们手动GC
,稍有不慎就会出现内存泄漏。 Unsafe
的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM
崩溃级别的异常,会导致整个JVM
实例崩溃,表现为应用程序直接崩掉。- 直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。
- 不受
-
代码示例:
public 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);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}
@Slf4j
@Data
class Student {
private volatile int id;
private volatile String name;
public static void main(String[] args) throws NoSuchFieldException {
Unsafe unsafe = UnsafeAccessor.getUnsafe();
Field id = Student.class.getDeclaredField("id");
Field name = Student.class.getDeclaredField("name");
// 获得成员变量的偏移量。
long idOffset = unsafe.objectFieldOffset(id);
long nameOffset = unsafe.objectFieldOffset(name);
Student student = new Student();
// 使用 cas 方法替换成员变量的值。
unsafe.compareAndSwapInt(student, idOffset, 0, 20);
unsafe.compareAndSwapObject(student, nameOffset, null, "张三");
log.debug("student={}", student);
// student=Student(id=20, name=张三)
}
}
8.2 解决最开始提出的问题
需求:通过
Unsafe
的CAS
方式,编写自定义的AtomicData
类并实现 Account 接口,最终完成模拟一千个取款的线程安全操作。
- 代码示例:
public class AtomicData {
/**
* 变量 - 使用 volatile 修饰。
*/
private volatile int data;
/**
* 操作底层的实例。
*/
private static final Unsafe UNSAFE = UnsafeAccessor.getUnsafe();
/**
* 偏移量。
*/
private static final long DATA_OFFSET;
static {
try {
Field field = AtomicData.class.getDeclaredField("data");
// 偏移量,用于 Unsafe 直接访问该属性。
DATA_OFFSET = UNSAFE.objectFieldOffset(field);
} catch (NoSuchFieldException e) {
throw new Error(e);
}
}
public AtomicData(int data) {
this.data = data;
}
public int getData() {
return this.data;
}
/**
* CAS 自减方法。
*
* @param amount 数量。
*/
public void decrease(int amount) {
while (true) {
int oldValue = data;
int newValue = oldValue - amount;
// cas 尝试修改 data 为 旧值 + amount,如果期间旧值被别的线程改了,返回 false。
if (UNSAFE.compareAndSwapInt(this, DATA_OFFSET, oldValue, newValue)) {
return;
}
}
}
public static void main(String[] args) throws InterruptedException {
// 使用自定义实现的 AtomicData 完成原子操作。
AtomicData atomicData = new AtomicData(1000);
Account.oneThousandUsersWithdraw(new Account() {
@Override
public Integer getBalance() {
return atomicData.getData();
}
@Override
public void withdraw(Integer amount) {
atomicData.decrease(amount);
}
});
// 第一次运行:balance:0,cost:50ms
// 第二次运行:balance:0,cost:49ms
}
}
- 总结:可以看到使用
Unsafe
直接操作底层的效率更高。
九、结束语
“-------怕什么真理无穷,进一寸有一寸的欢喜。”
微信公众号搜索:饺子泡牛奶。