Java并发编程volatile关键字
volatile理解
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和volatile 关键字机制。volatile具有synchronized关键字的“可见性”,volatile变量对于每次使用,线程都能得到当前volatile变量的最新值,但是没有synchronized关键字的“并发正确性”,也就是说不保证线程执行的有序性。
特性
1、保证内存可见性
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的。这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。
2、不保证原子性
原子性在一个操作是不可中断的,要么全部执行成功要么全部执行失败。如a++,a+=1就不是原子性操作,volatile不能保证原子性。
3、禁止指令重排序
计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排:
1. 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.
2. 处理器在进行重新排序是必须要考虑指令之间的数据依赖性
3. 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测
代码
保证内存可见性
package com.raicho.mianshi.myvolatile; public class MyVolatileVisibility { // private int i; private volatile int i; public void changeI(int i) { this.i = i; } public static void main(String[] args) { // System.out.println("没有加volatile关键字"); MyVolatileVisibility myVolatile = new MyVolatileVisibility(); new Thread(() -> { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } myVolatile.changeI(1); System.out.println(Thread.currentThread().getName()+"修改了i="+myVolatile.i); },"线程1 ").start(); System.out.println( Thread.currentThread().getName()+"访问 i = "+ myVolatile.i); while (myVolatile.i == 0) { } } }
当没有加volatile关键字时,当主线程先访问了i值为0,后线程1再进行修改i=1,回到mian线程,并不能察觉到i的值修改为1,然而一直在while循环不能结束,加上volatile关键字就能够检测到其他线程已经将i的值修改为1,结束程序。
不保证原子性
package com.raicho.mianshi.myvolatile; public class VolatileAtomicity { volatile int number = 0; public void addNum(){ number++; } public static void main(String[] args) { VolatileAtomicity va = new VolatileAtomicity(); for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { va.addNum(); } },String.valueOf(i)).start(); } //等等20条线程完成 while (Thread.activeCount() >2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+" number = "+va.number); } }
通过代码验证并最终number并不能达到20000,证明volatile并不保证原子性操作
解决方案
- 在addNum()方法上加锁synchronized关键字,肯定是可以解决的,但是synchronized加锁太重了,严重降低效率
- 使用AtomicInteger类
package com.raicho.mianshi.myvolatile; import java.util.concurrent.atomic.AtomicInteger; public class VolatileAtomicity { volatile int number = 0; public void addNum(){ number++; } AtomicInteger atomicInteger = new AtomicInteger(); public void addNumAtomicInteger(){ atomicInteger.getAndIncrement(); } public static void main(String[] args) { VolatileAtomicity va = new VolatileAtomicity(); for (int i = 0; i < 20; i++) { new Thread(()->{ for (int j = 0; j < 1000; j++) { //va.addNum(); va.addNumAtomicInteger(); } },String.valueOf(i)).start(); } //等等20条线程完成 while (Thread.activeCount() >2){ Thread.yield(); } // System.out.println(Thread.currentThread().getName()+" number = "+va.number); System.out.println(Thread.currentThread().getName()+" number = "+va.atomicInteger); } }
禁止指令重排序
public void mySort(){ int x=11;//语句1 int y=12;//语句2 x=x+5;//语句3 y=x*x;//语句4 }
重新排序后可能会变为
1234
2134
1324
问题:
请问语句4 可以重排后变成第一条码?
存在数据的依赖性 没办法排到第一个
单例模式中使用双重检测机制
public class SingletonDemo { private static volatile SingletonDemo instance=null; private SingletonDemo(){ System.out.println(Thread.currentThread().getName()+"\t 构造方法"); } /** * 双重检测机制 * @return */ public static SingletonDemo getInstance(){ if(instance==null){ synchronized (SingletonDemo.class){ if(instance==null){ instance=new SingletonDemo(); } } } return instance; } public static void main(String[] args) { for (int i = 1; i <=10; i++) { new Thread(() ->{ SingletonDemo.getInstance(); },String.valueOf(i)).start(); } } }
在多线程下,不加volatile关键字也可能出现指令重排的情况,是线程不安全的