环境及版本
并发三大性质
- 并发是计算机科学领域的重要概念,它涉及到多个任务或操作在同一时间段内执行的能力。并发有三大性质,分别是:原子性、有序性、可见性。下面我们谈一起来看看它们到底是什么:
原子性
- 原子性(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 原则来建立线程操作之间的先后关系。
volatile和synchronized具有不同的含义:
volatile 禁止了指令重排序。
synchronized 提供了互斥的含义,保证了多线程下临界区的有序执行,但临界区内部执行过程中可能会发生指令重排序。
可见性
- 可见性(Visibility):可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在多线程环境中,每个线程都有自己的工作内存,对共享变量的修改可能在一个线程的工作内存中进行,其他线程并不立即感知到这个修改。为了确保可见性,需要使用同步机制(如锁、volatile关键字)来保证共享变量的值在多个线程之间的可见性。
示例
public class CurrencyCharacter {
int cunt;
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++) {
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进行相关的处理,比如常见的禁止指令重排、可见性保证。