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

发布时间 2023-05-31 21:09:38作者: _mcj

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关键字
原子性 不能
可见性
有序性