线程安全之原子操作

举报
西魏陶渊明 发表于 2022/09/25 03:48:46 2022/09/25
【摘要】 作者: 西魏陶渊明 博客: https://blog.springlearn.cn/ (opens new window) 西魏陶渊明 莫笑少年江湖梦,谁不少年梦江湖 原子特性: 原子是最小的粒子,不可再分 这并不是一个化学课,而是巧妙的借用了化学上的一...

作者: 西魏陶渊明 博客: https://blog.springlearn.cn/ (opens new window)

西魏陶渊明

莫笑少年江湖梦,谁不少年梦江湖

原子特性: 原子是最小的粒子,不可再分

这并不是一个化学课,而是巧妙的借用了化学上的一个概念,即原子是最小的粒子,不可再分;原子操作也是不能再分的操作; 为了能把这个讲明白,下文基本都是大白话,其实Java本来并不是很难,而是总有一些人喜欢把简单的概念给复杂化。小编不喜欢 那种说辞,所以尽量简单易懂。如有问题,欢迎提出问题。共同交流进步,最后谢谢你的阅读。


# 举例说明原子操作重要性

在很多场景中我们需要我们的操作是原子特性的,如果我们写的程序都是单线程的,其实我们没必要考虑原子操作。但是假如 我们写多线程操作,或者是在Web服务中来更新对象属性,那么就必须要来考虑原子操作问题了。

举一个🌰例子A:

int a = 1;
   
1

可以看到程序对变量 a 操作,其实是有多个步骤进行的。在单线程环境下基本不会发生任何问题

举一个🌰例子B(单线程操作):


    
  1. public class Tester {
  2. private static Integer a = 1;
  3. private static AtomicInteger aa = new AtomicInteger(1);
  4. private static void restore() {
  5. a = 1;
  6. aa = new AtomicInteger(1);
  7. }
  8. public static void main(String[] args) {
  9. for (int i = 0; i < 10; i++) {
  10. test("第" + i + "次");
  11. restore();
  12. }
  13. }
  14. private static void test(String str) {
  15. for (int i = 1; i <= 1000; i++) {
  16. new Thread(() -> a = a + 1).start();
  17. new Thread(() -> aa.addAndGet(1)).start();
  18. }
  19. System.out.print(str + "常规操作a=" + a);
  20. System.out.println(" <===> "+str+"原子操作操作aa=" + aa);
  21. }
  22. }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

规律:


    
  1. /**
  2. * i i+1
  3. * 1: a = 1 + 1 = 2
  4. * 2: a = 2 + 1 = 3
  5. * 3: a = 3 + 1 = 4
  6. * 4: a = 4 + 1 = 5
  7. * 5: a = 5 + 1 = 6
  8. * 6: a = 6 + 1 = 7
  9. * 7: a = 7 + 1 = 8
  10. * 8: a = 8 + 1 = 9
  11. * 9: a = 9 + 1 = 10
  12. * 10:a = 10 + 1 = 11
  13. */
1 2 3 4 5 6 7 8 9 10 11 12 13

如上面代码变量a是基本类型,变量aa是原子类型,正常情况对a或者aa进行1000次操作结果都应该是 1001。正常情况我们可以理解是单线程操作。结果也是没有问题的。

举一个🌰例子C(多线程操作):


    
  1. public class Tester {
  2. private static Integer a = 1;
  3. private static AtomicInteger aa = new AtomicInteger(1);
  4. private static void restore() {
  5. a = 1;
  6. aa = new AtomicInteger(1);
  7. }
  8. public static void main(String[] args) throws Exception {
  9. for (int i = 0; i < 10; i++) {
  10. test("第" + i + "次");
  11. restore();
  12. }
  13. }
  14. private static void test(String str) throws Exception {
  15. for (int i = 1; i <= 100; i++) {
  16. new Thread(() -> a = a + 1).start();
  17. new Thread(() -> a = a + 1).start();
  18. new Thread(() -> aa.addAndGet(1)).start();
  19. new Thread(() -> aa.addAndGet(1)).start();
  20. Thread.sleep(1);
  21. }
  22. System.out.print(str + "常规操作a=" + a);
  23. System.out.println(" <===> " + str + "原子操作操作aa=" + aa);
  24. }
  25. }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

规律:


    
  1. /**
  2. * i 2 * i + 1
  3. * 1: a = 1 + 1 + 1 = 3
  4. * 2: a = 3 + 1 + 1 = 5
  5. * 3: a = 5 + 1 + 1 = 7
  6. * 4: a = 7 + 1 + 1 = 9
  7. * 5: 11
  8. * 6: 13
  9. * 7: 15
  10. * 8: 17
  11. * 9: 19
  12. * 10: 21
  13. */
1 2 3 4 5 6 7 8 9 10 11 12 13

多线程环境下操作会不会有问题呢? 出现了问题。我们看到使用常规操作的a变量出现了数据不一致情况。

实际上当循环的次数越多,出现错误的几率就越大,如下我们循环了1000次。

# 问题分析

我们思考为什么基本类型进行多线程操作时候会出现这种情况呢? 其实问题答案最开始已经说了。 我们通过这张图 就可以找到原因。

对变量的每次操作其实都有3个步骤

  1. 读取变量值
  2. 变量值操作
  3. 变量重新赋值。

我们模拟一下错误的原因。

当A线程读取a=1,并对1+1。但是还未对变量重新赋值a=2的时候, B线程也读取了A还未赋值的变量,此时变量还是1,那么B线程因为读取了还未更新的数据,所以也做1+1的操作。然后B对a 重新赋值了此时a=2,是B赋值的。这个时候A因为已经执行完了前两个步骤,最后也重新赋值了a=2。

这样数据就更新丢了。这就是因为数据更新不是原子性从而导致的问题。

因为数据更新丢了,所以出现了。

# 如何解决这种问题

如何解决这种问题,其实很简单只要我们保证我们的操作是原子操作即可,简单来说就是将更新的三个步骤合并成一个步骤即可,在Java中JDK已经为我们提供了很多的 原子操作每一个基本类型都对应一个原子操作。

# 原子基础类

原子基础类API

# 原子数组类

原子更新数组API

# 原子引用类

注意:

想要原子的更新字段,需要两个步骤:

1.每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

2.更新类的字段(属性)必须使用public volatile修饰符

# 最后我们看一下原子操作的原理

最后求关注,求订阅,谢谢你的阅读!

文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。

原文链接:springlearn.blog.csdn.net/article/details/125858011

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。