文章收录于Java并发编程专栏
Java代码经编译器编译后,生成的指令顺序可能与源代码的顺序有别,且编译器会将变量存于寄存器而非内存中。CPU处理器采用乱序或并行的方式来执行指令,且在执行过程中保存在CPU处理器本地缓存中的值,对于其他处理器是不可见的。
为了解决线程间的可见性和有序性问题,Java提供了volatile、synchronized 和 final 三个关键字,它们就像三个威力强大的法宝,助你轻松应对并发编程的挑战。
synchronized 关键字就像一位守护神,确保代码块在同一时刻只能被一个线程访问。可以理解成它是一把锁,锁住代码块,其他线程想要进入,就必须去争抢锁资源。这样就可以避免多个线程同时访问共享资源,造成数据不一致的问题。
synchronized作用于某段代码块,可以保证可见性和有序性。从JVM底层实现来看,synchronized代码块是由一对儿monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。
在Java 6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。
在Oracle JDK中,对JVM中的synchronized实现进行了很大的改进,增加了偏斜锁(Biased Locking)、轻量级锁和重量级锁,JVM检测到不同的竞争状况,就会采用锁的升级和降级来切换到最合适的锁实现,从而提高了性能。
volatile关键字就像一位轻骑兵,它保证变量的更新操作对其他线程立即可见,但不会阻止其他线程对变量的读写操作。最原始的意义就是禁用CPU缓存和编译优化,所以volatile关键字可以保证可见性和有序性。volatile相比sychronized关键字来说,是相对轻量级的同步机制。
volatile变量是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其它线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作进行与其它内存操作一起重排序。
简言之,volatile通过禁用 CPU 缓存和编译优化,以确保变量值始终是最新的。
int x = 2;
int y = 0;
volatile int i = 0;
int x = 3;
int y = 1;
以上代码中“i”变量使用volatile修饰,那么对这个变量的读写不能使用CPU缓存,必须从内存中读取或者写入。并且根据Happens-Before规则中的volatile规则,“i”变量前两个语句不会排到“i”变量后面,“i”变量后两个语句不会排在i变量前面。
虽然volatile可以在一定程度上保存可见性,但是不能滥用(能不用就不用)。只有当满足特定条件时才可使用,例如变量的每次赋值都是独立的,只有一个线程更新变量,访问变量不需要加锁等。
final关键字与volatile关键字正好相反,final修饰变量时会告诉编译器,这个变量生而不变可以使劲优化。final 定义类型及说明如下:
选择合适的同步机制需要根据具体场景进行判断。如果需要保证原子性和可见性,并且允许多个线程同时访问共享资源,则可以使用 volatile。如果需要保证原子性、可见性和有序性,并且只允许一个线程访问共享资源,则可以使用 synchronized。
当然,也可以将 volatile 和 synchronized 结合使用,例如,使用 volatile 保证变量的可见性,使用 synchronized 保证变量的原子性和有序性。
综上,volatile 关键字可以保证可见性和有序性,synchronized 关键字可以保证可见性和有序性,同时还可以保证原子性,final 关键字可以保证变量的不可变。在实际开发中,需要根据具体的需求选择合适的关键字来保证程序的正确性和性能。