在并发编程中,除了原子性和可见性,有序性也是很重要的一个特性。有序性指的是程序执行的顺序按照代码的先后顺序执行,对于单个线程来说,这是显而易见的,因为在单线程中,程序的执行顺序就是代码的排列顺序。
为什么有序性会成为问题?
在并发环境下,事情变得复杂起来。有两种类型的有序性问题需要考虑:
-
编译器优化带来的有序性问题:为了提高性能,编译器和处理器常常会对指令序列做重排序。重排序是在不改变单线程程序语义的情况下,通过改变指令的执行顺序来获取更优性能的一种技术。
-
内存系统的有序性问题:由于现代计算机使用了缓存系统,并且多处理器通常有自己的本地缓存,这可能会导致不同的处理器看到不一致的内存视图。
如何保证有序性?
Java Memory Model (JMM) 提供了一系列的内存屏障指令(happens-before规则)来保障有序性。
-
**
volatile
关键字:**当变量被声明为volatile后,编译器和运行时都会注意到这个变量是共享的,因此不会将这个变量上的操作与其他内存操作进行重排序。同时,volatile变量的写操作happens-before后续对同一个变量的读操作,保障了有序性。 -
**
synchronized
和Lock
机制:**在synchronized
同步块或方法内部,所有操作几乎是串行化执行的,同一时刻只允许一个线程进入代码块进行操作。释放锁的happens-before获取同一个锁。 -
**
final
关键字:**在Java内存模型中,被final修饰的字段,一旦初始化完成,并且构造函数退出,不允许对他们进行重排序。这保证了在访问一个final字段时,可以看到由构造函数对这个final字段的所有赋值操作。
代码举例
假设有以下代码:
int a = 10; // 语句1
int b = 20; // 语句2
如果这两条语句在没有同步的情况下被多线程访问,处理器或者JIT编译器可能会对这两条语句进行重排序,改变它们的执行顺序。而如果我们希望按照顺序执行,可以使用synchronized
关键字或volatile
关键字,如:
volatile int a = 10; // 语句1
volatile int b = 20; // 语句2
使用volatile
修饰后,语句1和语句2在内存中的顺序就如编码时的顺序一样,禁止进行指令重排序。
总的来说,有序性保证了程序在执行时,按照代码的预期顺序执行操作,而不会因为编译器或处理器优化导致执行顺序的混乱,这对于并发程序的正确性至关重要。