一、背景
一提到并发编程,我们便会想到volatile和synchronized这两个关键字,那么首先就要理解关于线程安全的两个名词:内存可见和执行顺序
- 内存可见:线程执行结果在内存中对其他线程的可见性。
- 执行顺序:控制代码的执行顺序及是否可以并发执行。
二、volatile
volatile解决的是内存可见问题
1.原理
基于CPU内存屏障指令实现的
内存屏障:通俗理解就是一种约定,当遇见它时不对他进行访问
2.volatile修饰的变量可见性
volatile是变量修饰符,其修饰的变量具有内存可见性
一般情况下,Java中为了加快程序的运行效率,会先把主存数据拷贝到线程本地(寄存器或是CPU缓存),操作完成后再把结果从线程本地缓存刷新到主存中,这样就会导致修改后放入变量结果同步到主存中需要一个过程,而此时另外的线程看到的还是修改之前的变量值,这样就会导致不一致
为了解决上述多线程中内存可见的问题,引入了 volatile 关键字,那么它为什么可以解决内存可见性问题呢?
答案: volatile 它会使得所有对 volatile 变量的读写都会直接读写主存,而不是先读写线程本地缓存,这样就保证了变量的内存可见性
3.volatile禁止指令重排
指令重排: 处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证各个语句的执行顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。指令重排序不会影响单个线程的执行,但是会影响到线程并发执行时的正确性
线程执行到volatile修饰变量的读写操作时,其他线程对这个变量的操作肯定已经完成了,且结果已经同步到了主存中,即对其他的线程可见,本线程再对该变量操作完全没有问题的
4.使用范围
volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,不能保证复合操作的原子性,比如 i++
i++,实际上是由三个原子操作组成:read i; inc; write i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但不能保证i结果的正确性,原因如下:
比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了,A和B线程同步到主存中的i的值都是1
5.使用场景
1、对变量的写入操作不依赖变量的当前值,或者只有单个线程更新变量的值
2、该变量没有包含在具有其他变量的不变式中
三、synchronized
synchronized既解决了内存可见性问题,又解决了执行顺序问题
synchronized 可以修饰代码块或方法,既可以保证可见性,又能够保证原子性
原子操作:一系列操作,要么都成功,要么都失败,一个失败则会引起全部都重新再来
1.原理
synchronized 是基于 monitor 实现的
2 synchronized 修饰的代码块或方法保证内存可见性
通过synchronized或者Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存中
3 synchronized 修饰的代码块或方法保证原子性
线程要么不执行(线程没有获取到对象锁),线程要么执行到底(线程获取到了对象锁),直到执行完释放锁
4 synchronized 使用范围
synchronized 不仅能修饰代码块,还可以修饰方法
5 synchronized 使用场景
需要控制多线程访问的方法或者更新的变量
四、volatile 和 synchronized 异同点
1.相同点
volatile 和 synchronized 都保证了内存可见性
2.不同点
1、volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、和类级别的
2、volatile仅能实现变量的修改可见性,不能保证原子性,而synchronized则可以保证变量的修改可见性和原子性
3、volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞
4、volatile标记的变量不会被编译器优化,而synchronized标记的变量可以被编译器优化
5、由于 4 中的区别,在某些情况下 volatile 的性能优于 synchronized