首页 > 编程语言 >Java并发之原子性、可见性和有序性

Java并发之原子性、可见性和有序性

时间:2023-05-31 21:11:48浏览次数:51  
标签:count Java 原子 并发 线程 内存 有序性 public

1.原子性

1.1 原子性的定义

原子性:原子性即是一个或者多个操作,要么全程执行,并且执行的过程中不被任何因素打断,要么全部不执行。

举个例子会更好理解:就像是我们去银行转账的时候,A给B转1000元,如果A的账户减少了1000之后,那么B的账户一定要增加1000。A的账户减钱,B的账户加钱,这两个操作就是原子操作,不会出现A的账户减钱,但B的账户没有加钱,A的账户没有减钱,B的账户加钱了这样的情形。

1.2 未保证原子性的样例

未保证原子性的样例这个我们可以通过count加1来进行测试:

// count增加类
public class AddCount {

    private int count = 0;

    public void addCount() {
        count++;
    }

    public int getCount() {
        return count;
    }

}

// 测试嗲用类
public static void main(String[] args) throws InterruptedException {
    AddCount addCount = new AddCount();
    for (int i = 0; i < 10000; i++) {
        new Thread(() -> {
            addCount.addCount();
        }).start();
    }
    // 让线程睡眠一段时间,等待计算执行完成
    Thread.sleep(5000);
    System.out.println(addCount.getCount());
}

运行结果:
image

分析:从运行结果可以看出其最终的执行结果为9999,并没有达到10000,这是因为数据加1操作并不是原子操作,未保证操作的原子性。
另外我们可以通过jdk自带的javap查看一下程序的指令码,这样我们就知道为什么其不是原子操作了:

Compiled from "AddCount.java"
public class com.mcj.transport.handler.AddCount {
  public com.mcj.transport.handler.AddCount();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field count:I
       9: return

  public void addCount();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field count:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field count:I
      10: return

  public int getCount();
    Code:
       0: aload_0
       1: getfield      #2                  // Field count:I
       4: ireturn
}

通过上面程序吗的addCount()方法可以看出,其大致可有分为三步:
1.将变量从内存中加载到CPU的寄存器中;
2.在CPU的寄存器中执行count++操作;
3.将count++后的结果写入缓存;
当其执行完第一步时发生线程切换的话,那么就会导致另一个线程执行的加1操作与本线程执行的加1操作进行重叠,从而导致最后的结果不正确。
具体的流程图如下:
image

1.3 如何保证原子性

  • synchronized修饰
  • lock加锁
  • 使用Atomic类
  • 利用CAS算法

2.可见性

2.1 可见性的定义

可见性:可见性是指一个线程修改了共享变量,那么其他线程能够立刻读到共享变量的最新值。

由于线程之间修改共享变量是将共享变量从主内存中复制一份在工作内存中,然后线程修改是直接在工作内存进行修改的,并不会立即同步到主内存上,所以当尚未同步到主内存时有另一个线程来读取该共享变量,其获取的值将不会是最新的值。这就是其没有保证该共享变量的可见性。
其修改流程如下所示:
image

2.2 未保证可见性的样例

这个可以通过看线程停止的时间来进行判断:

// 线程输出类
public class TestThread extends Thread {

    public boolean running = true;

    @SneakyThrows
    @Override
    public void run() {
        int n = 0;
        while (running) {
            n++;
            System.out.println(n + "hello");
        }
        System.out.println("stop");
    }

}

// 调用类
public static void main(String[] args) throws InterruptedException {
    TestThread testThread = new TestThread();
    testThread.start();
    testThread.sleep(1);
    testThread.running = false;
}

运行结果:
image

分析:由运行结果可以得知,其在运行了一百多条时才把数据同步到主内存中,需要注意的是,由于每次写入到主内存中时间不定,所以其每次运行结果值也是不确定的.
ps:加了volatile关键字之后,发现其与没有加之前速度差不多,这个则是因为JVM在下x86架构下,回写主内存的速度非常快,因此看着差距不大。

2.3 如何保证可见性

  • volatile关键字
  • synchronized关键字
  • lock锁

3.有序性

3.1 有序性的定义

有序性:有序性是指程序的执行顺序按照按照代码的先后顺序执行,不会出现跳过代码,或者改变代码执行顺序的情况。

由于一些操作并不是原子操作,比如使用双重检测机制创建单例对象,在new SingleInstance()时,其可以分为三个步骤:
1.先声明内存空间。
2.初始化。
3.将对象指向该内存空间
如果其执行的过程中无序,可能会导致2,3这两步骤反过来,也就是先指向内存空间,然后在初始化。此时如果线程A按照1,3,2的顺序执行,如果刚好执行到1,3,2中的3,此时正好线程B再次创建对象,由于线程A已经指向创建的内存空间了,所以线程B会得到一个未初始化的对象。

3.2 未保证有序性的样例

这里还是分析这个使用双创监测机制创建单例对象,具体的代码如下:

public class SingleInstance {

    private static SingleInstance instance;

    private SingleInstance(){};

    public static SingleInstance getInstance(){
        if(instance == null){
            synchronized (SingleInstance.class){
                if(instance == null){
                    instance = new SingleInstance();
                }
            }
        }
        return instance;
    }

}

上面的代码就可能会出现3.1中描述的那种情况,这里自己跑程序并没有跑出来。具体的流程可以从下面流程图看出,详细可以看高并发编程:核心原理与案例实战:
image

3.3 如何保证有序性

  • synchronized关键字
  • lock锁
  • volatile关键字

4.总结

lock锁 synchronized关键字 volatile关键字
原子性 不能
可见性
有序性

标签:count,Java,原子,并发,线程,内存,有序性,public
From: https://www.cnblogs.com/mcj123/p/17441239.html

相关文章

  • ThreadLocal 详解【并发容器】
    ThreadLocal是什么?有哪些使用场景?ThreadLocal是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap对象,简单说ThreadLocal就是一种以空间换时间的做法,每个线程可以访问自己内部ThreadLocalMap对象内的value。通过这种方式,避免资源在多线程间共享。原理:......
  • Java 微服务中的聚合器设计模式示例
    微服务架构中的聚合器设计模式是一种设计模式,用于通过聚合多个独立的微服务的响应来组成一个复杂的服务。它也是与SAGA、CQRS和EventSourcing一起的基本微服务设计模式之一。当客户端请求需要跨多个微服务分布的数据或功能时,此模式是合适的。可以提高系统的性能和可扩展性通过允许......
  • 揭开 JavaScript 事件循环的神秘面纱
    Javascript是一种单线程语言,这意味着它一次只能执行一个任务。但是,它仍然设法同时执行多项任务。它通过使用一些复杂的数据结构给人一种多线程的错觉。为实现这一点,Javascript引擎有一个称为事件循环的重要组件。我们将了解什么是事件循环以及它如何在不阻塞主线程的情况下处理异......
  • Java script事件问题
    鼠标事件:/*onclick单击*/  /*ondbclick双击*/  /*onmouseover*/  /* div1.onclick=function(){    console.log('单击')  }  div1.ondbcolick=function(){    console.log('双击')  }*/ 表单事件://onsubmit事件......
  • Java商超管理系统
    该商超管理系统是为了能够帮助超市进行更加有效的人员管理,更加高效的完成购物结算以及库存管理,使用Java来模拟设计该系统统一进行商品信息和职员信息的管理,大大降低人工和时间成本。该系统在之前学习的java知识体系中运用到文件流、面向对象、集合、泛型等诸多知识,是一个知识更加综......
  • 函数式编程和java
    函数式编程和java在计算机科学中,函数式编程是一种编程范式,通过应用和组合函数来构建程序。它是一种声明式编程范式(对应命令式编程),其中函数定义是将数值映射到其他数值的表达式树,而不是更新程序运行状态的命令式语句序列。函数的定义数学上的函数是自变量到因变量的映射关系,......
  • java 中字符型 和 字符串类型有什么区别
    在Java中,字符型和字符串类型都是常用的数据类型,但是它们有着本质的不同。字符型是基本数据类型,表示单个字符,使用char表示。例如:'A'、'1'、'中'等。字符串类型是引用数据类型,表示由多个字符组成的字符串,使用String表示。例如:"hello"、"world"、"你好"等。下面列举一些它们......
  • 关于第一次学习JavaScript程序调试心得
    源程序如上,源代码来源(刘永富博士-ExcelVBA编程开发下册)。运行之后,网页无反应,alert不弹窗。经查询https://www.runoob.com/jsref/event-body-onload.htmlhttps://blog.csdn.net/sinat_29398599/article/details/65450485需添加onload事件。Bodyonload事件,onload事件在页......
  • 关于Java中的String类
    我们知道String声明的字符串是不能被改变的。那么如果我们使用下面的语句:Stringstr="Hello";str=str+"World!";你会发现,我们如果输出str,答案是:HelloWorld!那么,从表面上看str被改变了,而实际上是这样的:当我们执行上面的两条语句后,中间的过程用如下图来表示:也就是说开始用Str......
  • JavaScript中的Hook技术:特性、优点、缺点和使用场景
    引言:随着JavaScript的不断发展,开发者们正在寻找更灵活和可扩展的方式来修改或扩展现有的代码。其中一种广泛应用的技术是"Hook",它允许开发者拦截和修改现有的函数或方法的行为。本文将详细介绍JavaScript中的Hook技术,包括其特性、优点、缺点和使用场景,并提供示例代码进行说明。什么......