【Java&Go并发编程系列】8.原子操作——java.util.concurrent.atomic VS sync.atomic

说明:Java & Go 并发编程序列的文章,根据每篇文章的主题或细分标题,分别演示 Java 和 Go 语言当中的相关实现。更多该系列文章请查看:Java & Go 并发编程系列

原子操作,通过底层硬件的支持,来保证整个操作的原子性,所以原子操作也是并发安全的。Java 和 Go 语言都提供了支持原子操作的并发工具类,分别在 java.util.concurrent.atomicsync.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 并发编程系列

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值