Java 多线程三大核心
原子性
Java
的原子性就和数据库事物的原子性差不多,一个操作中要么全部执行成功或者失败。
JMM
只是保证了基本的原子性,但类似于 i++
之类的操作,看似是原子操作,其实里面涉及到:
- 获取 i 的值。
- 自增。
- 再赋值给 i。
这三步操作,所以想要实现 i++
这样的原子操作就需要用到 synchronize
或者是 lock
进行加锁处理。
如果是基础类的自增操作可以使用 AtomicInteger
这样的原子类来实现(其本质是利用了 CPU
级别的 的 CAS
指令来完成的)。
其中用的最多的方法就是: incrementAndGet()
以原子的方式自增。
源码如下:
1 | public final long incrementAndGet() { |
首先是获得当前的值,然后自增 +1。接着则是最核心的 compareAndSet()
来进行原子更新。
1 | public final boolean compareAndSet(long expect, long update) { |
其逻辑就是判断当前的值是否被更新过,是否等于 current
,如果等于就说明没有更新过然后将当前的值更新为 next
,如果不等于则返回false
进入循环,直到更新成功为止。
还有其中的 get()
方法也很关键,返回的是当前的值,当前值用了 volatile
关键词修饰,保证了内存可见性。
1 | private volatile int value; |
可见性
现代计算机中,由于 CPU
直接从主内存中读取数据的效率不高,所以都会对应的 CPU
高速缓存,先将主内存中的数据读取到缓存中,线程修改数据之后首先更新到缓存,之后才会更新到主内存。如果此时还没有将数据更新到主内存其他的线程此时来读取就是修改之前的数据。
如上图所示。
volatile
关键字就是用于保证内存可见性,当线程A更新了 volatile 修饰的变量时,它会立即刷新到主线程,并且将其余缓存中该变量的值清空,导致其余线程只能去主内存读取最新值。
使用 volatile
关键词修饰的变量每次读取都会得到最新的数据,不管哪个线程对这个变量的修改都会立即刷新到主内存。
synchronize
和加锁也能能保证可见性,实现原理就是在释放锁之前其余线程是访问不到这个共享变量的。但是和 volatile
相比开销较大。
顺序性
以下这段代码:
1 | int a = 100 ; //1 |
正常情况下的执行顺序应该是 1>>2>>3
。但是有时 JVM
为了提高整体的效率会进行指令重排导致执行的顺序可能是 2>>1>>3
。但是 JVM
也不能是什么都进行重排,是在保证最终结果和代码顺序执行结果一致的情况下才可能进行重排。
重排在单线程中不会出现问题,但在多线程中会出现数据不一致的问题。
Java 中可以使用 volatile
来保证顺序性,synchronize 和 lock
也可以来保证有序性,和保证原子性的方式一样,通过同一段时间只能一个线程访问来实现的。
除了通过 volatile
关键字显式的保证顺序之外, JVM
还通过 happen-before
原则来隐式的保证顺序性。
其中有一条就是适用于 volatile
关键字的,针对于 volatile
关键字的写操作肯定是在读操作之前,也就是说读取的值肯定是最新的。
volatile 的应用
双重检查锁的单例模式
可以用 volatile
实现一个双重检查锁的单例模式:
1 | public class Singleton{ |
这里的 volatile
关键字主要是为了防止指令重排。
如果不用 volatile
,singleton = new Singleton();
,这段代码其实是分为三步:
- 分配内存空间。(1)
- 初始化对象。(2)
- 将
singleton
对象指向分配的内存地址。(3)
加上 volatile
是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。
控制停止线程的标记
1 | private volatile boolean flag ; |
这里如果没有用 volatile 来修饰 flag ,就有可能其中一个线程调用了 stop()
方法修改了 flag 的值并不会立即刷新到主内存中,导致这个循环并不会立即停止。
这里主要利用的是 volatile
的内存可见性。
总结一下:
volatile
关键字只能保证可见性,顺序性,不能保证原子性。