Java多线程
Synchronized和Volatile
-
volatile
volatile是JVM提供的轻量级同步机制,是线程不安全的.volatile保证了可见性和有序性.
volatile的作用
-
保证内存可见性
假设变量a已经从主内存中存储到了工作内存中,那么在内存中对变量a的值进行修改,工作内存中的变量
a不会立刻更新.而volatile修饰的变量可以保证工作内存中的值是最新的(和主内存中的变量值一致)
volatile保证内存可见性的原理:JVM会对添加了volatile关键字修饰的变量添加一条lock前缀指令.lock前缀指令在多核处理器中会做2件事情:将当前工作内存中的变量的值写回到主内存中;写回操作会引起其他处理器中缓存了该数据的地址无效.
因此当某个变量被volatile修饰时,该当前CPU中的最新的变量写回到主内存中,同时为了保证多个处理器的缓存一致,就会实现缓存一致性协议,每个处理器会通过检查主内存中的数据来判断自己缓存的数据是否已经过期,如果发现两个数据的地址不一致,则认为当前缓存的数据的地址失效,之后该处理器在处理该数据时由于该数据已经无效所以会从主内存中读取该数据的最新值.
static int a = 1; public static void main(String[] args){ Thread t1 = new Thread(){ @Override public void run(){ int i = 1; while(a == 1){ i++; } } } Thread t2 = new Thread(){ @Override public void run(){ a = 2; } } t1.start(); Thread.sleep(1000); t2.start(); } // volatile适用于变量在一个线程中只读取,在另一个线程中修改的场景 // 上述程序当不添加volatile关键字,程序会无限执行下去,原因是因为t2线程对a变量作的修改t1线程看不到,t1线程每次读取的仍是自己工作内存中存储的a的旧值1. // 当添加了volatile关键字后,t2线程对a变量作的修改会同步到主内存上,同时会让t1线程工作内存内存放a变量的地址失效,从而再下一次读取的时候从主内存中读取到a的最新值
-
禁止指令重排序
volatile实现禁止指令重排序是依靠JMM在编译器和处理器层面限制指令重排序,而JMM的内存屏障策略是保守策略
LoadLoadBarriers:该屏障之前的语句中数据的读取优先于该屏障之后所有语句中数据的读取
StoreStoreBarriers:该屏障之前的语句中数据的存储优先于该屏障之后所有语句中数据的存储
LoadStoreBarriers:该屏障之前的语句中数据的读取优先于该屏障之后所有语句中数据的存储
StoreLoadBarriers:该屏障之前的语句中数据的存储优先于该屏障之后所有语句中数据的读取
volatile的使用情况
- 当存在两个及以上的线程访问同一个变量时使用volatile,当要访问的变量已在synchronized代码块中或已经是常量时无须用volatile修饰
- volatile修饰的变量禁止了编译器中对代码的优化,所以执行效率低,必要时再使用
-
-
synchronized
synchronized可以保证线程的安全,添加synchronized的代码块保证在同一时刻只能被一个线程执行,一旦一个线程执行持有synchronized的代码块后,其他线程想要处理该代码块就需要等待已经持有的线程释放锁,否则处于阻塞状态.除此之外,synchronized还可以保证持有该锁的线程对代码块的修改可以被其他线程共享,也就是synchronized可以完全代替volatile来保证可见性
synchronized的原理
在JVM中,每一个对象都有一个对象头,而对象头的Mark Word中记录着该对象持有的锁类型