【多线程系列】终于理解了多线程中不得不谈的并发三大性质

Java社区

环境及版本

  • 运行版本:JDK 1.8

并发三大性质

  • 并发是计算机科学领域的重要概念,它涉及到多个任务或操作在同一时间段内执行的能力。并发有三大性质,分别是:原子性、有序性、可见性。下面我们谈一起来看看它们到底是什么:

原子性

  • 原子性(Atomicity):原子性是指一个操作是不可分割的整体,要么完全执行,要么完全不执行,不存在中间状态。
  • 不同于数据库事务原子性,在并发编程中,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。即保证一系列操作,不可以被拆分执行,执行过程中,需要互斥排它,不能有其他线程执行这块临界区。常见的原子操作包括加锁、解锁、读取、写入等。
  • Java内存模型(Java Memory Model,JMM)定义了8种原子操作,它们用于定义多线程环境下的内存访问行为。这些操作包括以下8种:
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
  • 除了上述的8种原子操作,还可以使用Atomic 类、使用锁(例如 Synchronized 关键字或 Lock)实现原子操作。
如何理解 Synchronized 原子性:
由于只有一个线程能够执行临界区中的代码,synchronized 关键字确保了这段代码的原子性、有序性,临界区内的操作可以被视为一个整体,要么完全执行,要么不执行,不会出现中间的不一致状态。

有序性

  • 有序性(Ordering):有序性是指程序执行的结果按照一定的规则,符合预期的顺序。指令重排序可以保证单线程串行语义一致(实际执行顺序不一定和代码顺序相同),但是没有义务保证多线程间的语义也一致,因此多线程环境中,由于指令重排序和线程的交替执行,程序的执行顺序可能与代码的编写顺序不完全一致。为了保证有序性,需要使用同步机制(如锁、volatile关键字)或者使用 happens-before 原则来建立线程操作之间的先后关系。
volatilesynchronized具有不同的含义:
volatile 禁止了指令重排序。
synchronized 提供了互斥的含义,保证了多线程下临界区的有序执行,但临界区内部执行过程中可能会发生指令重排序。

可见性

  • 可见性(Visibility):可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在多线程环境中,每个线程都有自己的工作内存,对共享变量的修改可能在一个线程的工作内存中进行,其他线程并不立即感知到这个修改。为了确保可见性,需要使用同步机制(如锁、volatile关键字)来保证共享变量的值在多个线程之间的可见性。

示例

public class CurrencyCharacter {
    /**
     * 当 线程 对 count 进行修改时,会将主内存的数据读拷贝到工作内存进行操作
     * 不同的线程都有自己的副本,线程之间对副本的修改可见性不会被保证
     */
    int cunt;

    /**
     * except return value 20000
     *
     * @return
     * @throws InterruptedException
     */
    public int addCurrency() throws InterruptedException {
        int loop = 10000;
        Thread thread1 = new Thread(getRunnable(loop));
        Thread thread2 = new Thread(getRunnable(loop));
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        return cunt;
    }

    private Runnable getRunnable(int loop) {
        return () -> {
            for (int i = 0; i < loop; i++) {
                // count ++ 并不是原子性 简单可以分为使用、赋值+1、存储三步操作
                this.cunt++;
            }
        };
    }
}

class CurrencyCharacterTest {

    @Test
    void addCurrency() throws InterruptedException {
        CurrencyCharacter currencyCharacter = new CurrencyCharacter();
        Assertions.assertNotEquals(20000, currencyCharacter.addCurrency());
    }

    @Test
    void order() {
        int a = 1;
        int b = 2; // 前面两行的执行顺序并不会影响执行结果,因此可以进行指令重排序
        int c = a + b;
        System.out.println(c);
    }
}

总结

  • 从上述我们可以看到,缓存带来了可见性问题,多线程带来了原子性问题,编译优化(编译器、CPU)、缓存带了有序性问题,最简单的方式我们可以静止缓存,编译器、处理器指令重排优化,但是程序的性能将会大大降低。
  • 为了解决这个问题,JMM提供了一些规范,一方面让开发者可以更加方便的进行多线程编程,而不需要了解过多的了解计算机底层的实现,一方面基于这些规则要求编译器、CPU进行相关的处理,比如常见的禁止指令重排、可见性保证。
0
0
0
0
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论