聊一聊并发编程三要素

244 阅读5分钟

单线程在程序执行时,代码串行执行,前面的代码先于后面的执行,需要在上一个任务完成后才能开始新的任务,效率比较低。为了提高处理器资源的利用率提高系统的吞吐率,基本上都采用多线程和并发的运作方式。

简述并发三要素

原子性
在一个操作中,CPU 不可以在中途暂停然后再调度,即不被中断操作,要么执行完成,要么就不执行。
可见性
多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
有序性
程序执行的顺序按照代码的先后顺序执行。

如何保证原子性

原子性操作指相应的操作是单一不可分割的操作。
例如:对int变量count执行count++操作就不是原子性操作。因为count++实际上可以分解为3个操作:

  1. 读取变量count的当前值;
  2. 拿count的当前值和1做加法运算;
  3. 将加完后的值赋给count变量。

在多线程环境中,非原子操作可能会受其他线程的干扰。synchronized关键字可以实现操作的原子性,其实质是:通过该关键字所包括的临界区(Critical Section)的排他性保证在任何一个时刻只有一个线程能够执行临界区中的代码,这使得临界区中的代码代表了一个原子操作。

如何保证内存可见性

CPU在执行代码的时候,为了减少变量访问的时间消耗可能将代码中访问的变量的值缓存到该CPU缓存区中,因此相应的代码再次访问该变量的时候,相应的值可能从CPU缓存中而不是主内存中读取的。同样的,代码对这些被缓存过的变量的值的修改也可能仅是被写入CPU缓存区,而没有写入主内存。由于每个CPU都有自己的缓存区,因此一个CPU缓存区中的内容对于其他CPU而言是不可见的。这就导致了在其他CPU上运行的其他线程可能无法“看到”该线程对某个变量值的更改。这就是所谓的内存可见性。
synchronized关键字的另一个作用就是保证了一个线程执行临界区中的代码时,所修改的变量值对于稍后执行该临界区的线程来说是可见的。这对于保证多线程代码的正确性来说非常重要。
volatile关键字也能够保证内存可见性。即一个线程对一个采用volatile关键字修饰的变量的值的更改,对于其他访问该变量的线程而言总是可见的。
volatile关键字实现内存可见性的核心机制是:当一个线程修改了一个volatile修饰的变量的值时,该值会被写入主内存(即RAM)而不仅仅是当前线程所在的CPU的缓存区,而其他CPU的缓存区中存储的该变量的值也会因此而失效(从而得以更新为主内存中该变量的“新值”)。这就保证了其他线程访问该volatile修饰的变量时,总是可以获取到该变量的最新值。

如何保证有序性(避免指令重排序) 

   编译器和CPU为了提高指令的执行效率可能会进行指令重排序,这使得代码的实际执行方式可能不是按照我们所认为的方式进行。例如下面的实例变量初始化语句:

private SingleTon instance = new SingleTon();

正常流程:

  1. 分配一段用于存储SingleTon 实例的内存空间;
  2. 创建类SingleTon 的实例;
  3. 将对该内存空间的引用赋给变量instance;

但是由于指令的重排序作用,这段代码的实际执行顺序可能是:

  1. 分配一段用于存储SingleTon 实例的内存空间;
  2. 将对该内存空间的引用赋给变量instance;
  3. 创建类SingleTon 的实例;

因此,当其他线程访问instance变量的值时,其得到的仅是指向一段存储SingleTon 实例的的内存空间的引用而已,而该内存空间相应的SingleTon 实例的初始化可能尚未完成,这就可能导致一些意想不到的结果。而禁止指令重排序则是可以使得上述代码按照我们所期望的顺序来执行。

注意:不要将volatile用在getAndOperate场合(这种场合不原子,需要再加锁),仅仅set或者get的场景是适合volatile的

Volatile、synchronized两者的区别联系

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
    synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
  2. volatile仅能使用在变量级别;
    synchronized则可以使用在变量、方法、和类级别的。
  3. volatile仅能实现变量的修改可见性,不能保证原子性(线程A修改了变量还没结束时,另外的线程B可以看到已修改的值,而且可以修改这个变量,而不用等待A释放锁,因为Volatile 变量没上锁);
    而synchronized则可以保证变量的修改可见性和原子性。
  4. volatile不会造成线程的阻塞;
    synchronized可能会造成线程的阻塞和上下文切换。
  5. volatile标记的变量不会被编译器优化;
    synchronized标记的变量可以被编译器优化。