说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列
原子操作,通过底层硬件的支持,来保证整个操作的原子性,所以原子操作也是并发安全的。Java 和 Go 语言都提供了支持原子操作的并发工具类,分别在 java.util.concurrent.atomic
和 sync.atomic
包里面。本文介绍其中两种比较常用的操作:原子加法和比较并交换CAS(compare and swap)
原子加法
「Java」AtomicLong
下面代码使用 AtomicLong 来实现原子自增(加1)。使用100个线程,每个线程执行100次,看看运行的结果是否为10000。
AtomicLong atomicLong = new AtomicLong();
int numberOfThreads = 100;
Thread[] threads = new Thread[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
atomicLong.incrementAndGet();
}
});
threads[i].start();
}
// 等待线程结束
for (int i = 0; i < numberOfThreads; i++) {
threads[i].join();
}
System.out.printf("最终输出值: %d\n", atomicLong.get());
运行结果:
最终输出值: 10000
Java 中针对不同的类型,提供了相对应的原子操作工具类,比如 AtomicBoolean 和 AtomicInteger。从 JDK 8 开始,还提供了 LongAdder,如果只是执行累加操作,可以使用 LongAdder 获得更好的性能。
LongAdder longAdder = new LongAdder();
int numberOfThreads = 100;
Thread[] threads = new Thread[numberOfThreads];
for (int i = 0; i < numberOfThreads; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 100; j++) {
longAdder.increment();
}
});
threads[i].start();
}
// 等待线程结束
for (int i = 0; i < numberOfThreads; i++) {
threads[i].join();
}
System.out.printf("最终输出值: %d\n", longAdder.sum());
「Go」atomic.AddInt64
启用100个 Goroutine, 每个 Goroutine 执行100次原子自增1。
// number 为 64位的int类型
number := int64(0)
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
for j := 0; j < 100; j++ {
// 自增步长为1,也可以是负数
atomic.AddInt64(&number, 1)
}
}()
}
// 等待启用的100个 Goroutine 结束
wg.Wait()
fmt.Printf("最终输出值: %d\n", number)
运行结果:
最终输出值: 10000
针对不同的数据类型,Go 语言中支持原子操作 Add 的方法还有AddInt32、AddUint32、AddUint64等。
比较并交换CAS
CAS(compare and swap),对于一个变量,只有当变量的值等于预期的值时,才会把变量的值替换为新值,并返回旧值。否则什么都不做。
代码场景:基于 CAS 粗略实现一个类似于 git 的版本控制系统。假设使用自增的数字表示版本号,初始版本号为0。提交操作必须包含当前的版本号,返回新的版本号。如果客户端提交的当前版本号与服务端的当前版本号不一致,则不允许提交,需要客户端执行 merge 或 rebase 操作之后再提交。
「Java」AtomicLong#compareAndSet
Java中 AtomicLong 的 compareAndSet 跟 compare and swap 的主要区别只是返回值不通过,compareAndSet 返回是否交换成功, compare and swap 返回交换后的值。
/*
* 表示一个版本控制系统
*/
static class VersionControl {
// 使用 atomicLong 提供的 compareAndSet 来保证版本的准确性
private AtomicLong version;
private List<String> history;// 使用list来保存提交历史记录
// 版本初始化
public long init() {
this.version = new AtomicLong();
history = new ArrayList<>(8);
return version.get();
}
// 基于上一个版本号,提交成功返回新的版本好号
public long commit(long preVersion, String commitMessage) {
long newVersion = preVersion + 1;
if (!version.compareAndSet(preVersion, newVersion)) {
// 简单地抛出运行时异常表示提交失败
throw new RuntimeException("版本提交失败");
}
history.add(commitMessage);
return newVersion;
}
// 查看提交历史
private void printLog() {
history.stream()
.map(e -> "|-" + e)
.sorted(Comparator.reverseOrder())
.forEach(System.out::println);
}
}
public static void main(String[] args) throws InterruptedException {
System.out.println("演示正常的提交:");
normalCommit();
System.out.println("演示有冲突的提交:");
conflictingCommit();
}
private static void normalCommit() {
VersionControl vc = new VersionControl();
long v0 = vc.init();
long v1 = vc.commit(v0, "First commit.");
long v2 = vc.commit(v1, "Second commit.");
vc.commit(v2, "Third commit.");
vc.printLog();
}
private static void conflictingCommit() throws InterruptedException {
VersionControl vc = new VersionControl();
long v0 = vc.init();
long v1 = vc.commit(v0, "First commit.");
// 模拟两个人同时基于版本v1做提交, 只有一个人会成功
Thread t1 = new Thread(() -> vc.commit(v1, "Second commit."));
Thread t2 = new Thread(() -> vc.commit(v1, "Second commit."));
t1.start();
t2.start();
// 等待t1、t2线程结束
t1.join();
t2.join();
}
运行结果:
演示正常的提交:
|-Third commit.
|-Second commit.
|-First commit.
演示有冲突的提交:
Exception in thread "Thread-1" java.lang.RuntimeException: 版本提交失败
at ...
「Go」atomic.CompareAndSwapInt64
Go 语言版本的实现基于 atomic.CompareAndSwapInt64 函数。该函数的返回值为 bool 类型,表示是否交换成功。
// VersionControl 版本控制
type VersionControl struct {
version int64
history []string
}
// 版本初始化,返回初始版本
func (vc *VersionControl) init() int64 {
return vc.version
}
// 基于上一个版本号,提交成功返回新的版本号
func (vc *VersionControl) commit(preVersion int64, commitMessage string) int64 {
newVersion := preVersion + 1
if !atomic.CompareAndSwapInt64(&vc.version, preVersion, newVersion) {
// 简单的使用 panic 函数制造异常
panic("版本提交失败")
}
vc.history = append(vc.history, commitMessage)
return newVersion
}
// 查看提交历史
func (vc *VersionControl) printLog() {
for i := len(vc.history) - 1; i >= 0; i-- {
fmt.Printf("|-%s\n", vc.history[i])
}
}
func main() {
fmt.Println("演示正常的提交:");
normalCommit();
fmt.Println("演示有冲突的提交:");
conflictingCommit()
}
func normalCommit() {
vc := VersionControl{}
v0 := vc.init()
v1 := vc.commit(v0, "First Commit.")
v2 := vc.commit(v1, "Second Commit.")
vc.commit(v2, "Third Commit.")
vc.printLog()
}
func conflictingCommit() {
vc := VersionControl{}
v0 := vc.init()
v1 := vc.commit(v0, "First Commit.")
go func() {
v2 := vc.commit(v1, "Second Commit.")
_ = v2
}()
go func() {
v2 := vc.commit(v1, "Second Commit.")
_ = v2
}()
time.Sleep(time.Millisecond * 100)
}
运行结果:
演示正常的提交:
|-Third Commit.
|-Second Commit.
|-First Commit.
演示有冲突的提交:
panic: 版本提交失败
goroutine 6 [running]:
main.(*VersionControl).commit(...)
针对不同的数据类型,Go 语言同样提供了 CompareAndSwapInt32、CompareAndSwapUint32、CompareAndSwapUint64 等。
拓展
使用CAS和自旋来解决并发问题
compare and swap 是原子操作中比较重要的机制,甚至前面讲到的原子自增,也可以基于 CAS 来实现。
Java 1.7 AtomicLong 的源码
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final long incrementAndGet() {
for (;;) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
使用 CAS 一般会结合自旋(如上所示的 for 循环)来解决并发问题,该模式也是并发编程里面实现无锁操作的典型用法。
更多该系列文章请查看:Java & Go 并发编程系列