volatile关键字16问
1.volatile 关键字是什么意思?
volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。
2.你了解到的什么地方使用了 volatile 关键字?解决了什么问题?
在juc包下的大量的类都使用到了volatile关键字,volatile关键字解决线程变量的可见性问题。
3.volatile 和 JMM 有什么关系?
每个线程读取和存储的都是线程的工作内存。而线程的工作内存再到主存中的存储是肯定会有一些时差的。也就是改变了一个变量的值之后,另一个调用这个变量的对象是不能马上知道的。如果说要让其他线程立即可见这个改动,就要使用volatile关键字修饰。一旦使用这个关键字之后,所有调用这个变量的线程就直接去主存当中拿取数据。每个线程不能访问其他线程的工作内存。
4.什么是指令重排?volatile 和指令重排有什么关系?
指令重排:指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序员带来问题。
在JDK1.5之后,可以使用volatile变量禁止指令重排序。 volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
5.什么是内存屏障?volatile 和内存屏障有什么关系?
内存屏障是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
volatile是通过内存屏障实现可见性的。
6.什么是 happens-before?volatile 和它有什么关系?
因为jvm会对代码进行编译优化,指令会出现重排序的情况,为了避免编译优化对并发编程安全性的影响,需要happens-before规则定义一些禁止编译优化的场景,保证并发编程的正确性。
访问volatile变量在语句间建立了happens-before关系。当写入一个volatile变量时,它与之后的该变量的读操作建立了happens-before关系。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
简单来说,就是在volatile写之后加入了一个StoreLoad屏障,防止后面的读与前面的写重排序了。这样后面的线程读到的,就是一个完整的对象。
7.如果单 CPU 的服务器,是否使用 volatile 对程序有影响吗?
即使是单核, 也有可能一个线程正在执行某个函数体的时候, CPU开小差跑去执行另一个线程了的函数了, 所以还是有影响的。
8.两条语句,第一条是普通写,第二条是 volatile 写,其他线程对第一条普通写可见吗?
9.volatile int i;i++ 操作会有线程安全问题吗?
会有的,volatitle保证了线程变量的可见性,但是不保证原子性。
10.volatile 能否替代 CAS?
不可能,cas保证了原子性。
11.为什么 AQS 里面的 state 使用了 CAS 还需要 volatile?
因为需要volatile的保证state的线程变量可见性。
12.Unsafe.putOrderedObject 是什么?能否替代 volatile?
Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedInt,Unsafe. putOrderedLong这三个方法,JDK会在执行这三个方法时插入StoreStore内存屏障,避免发生写操作重排序。而在Intel 64/IA-32架构下,StoreStore屏障并不需要,Java编译器会将StoreStore屏障去除。比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外,不会带来其它的性能开销。
13.可以认为 CAS + volatile = synchronized 吗?
不可以,因为cas操作存在一些问题。
ABA问题
1 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
2 循环时间长开销大
一般CAS操作都是在不停的自旋,这个操作本身就有可能会失败的,如果一直不停的失败,则会给CPU带来非常大的开销。
3 只能保证一个共享变量的原子操作
看了CAS的实现就知道这个只能针对一个共享变量,如果是多个共享变量就只能使用synchronized。除此之外,可以考虑使用AtomicReference来包装多个变量,通过这种方式来处理多个共享变量的情况。
14.解决可见性问题,使用了 synchronized 还需要 volatile 吗?
需要。
一方面是因为synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并不是锁,所以不存在阻塞和性能问题。
另外一方面,因为volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,所以在有些场景中是可以避免发生指令重排的问题的。
https://www.cnblogs.com/hollischuang/p/11386988.html 可见此篇博文。
15.利用 volatile 手写一个懒汉式单例模式,并解释为什么这么写。
class SingletonClass{ private volatile static SingletonClass instance = null; private SingletonClass() {} public static SingletonClass getInstance() { if(instance==null) { synchronized ( SingletonClass.class) { if(instance==null) instance = new SingletonClass();//语句1 } } return instance; } }
语句1不是一个原子操作,在jvm中是三个操作。
1.给instance分配空间、
2.调用 Singleton 的构造函数来初始化、
3.将instance对象指向分配的内存空间(instance指向分配的内存空间后就不为null了);
在JVM中的及时编译存在指令重排序的优化,也就是说不能保证1,2,3执行的顺序,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
通过添加volatile就可以解决这种报错,因为volatile可以保证1、2、3的执行顺序,没执行玩1、2就肯定不会执行3,也就是没有执行完1、2instance一直为空,
16.使用 volatile 手写一个生产者消费者程序。
public class Data { private String id; private String name; public Data(String id,String name){ this.id = id; this.name = name; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Data [id=" + id + ", name=" + name + "]"; } }
public class Provider implements Runnable{ //共享缓冲区 private BlockingQueue<Data> queue; //多线程间是否启动变量,有强制从主内存中刷新的功能,及时返回线程状态 private volatile boolean isRunning = true; //id生成器 private static AtomicInteger count = new AtomicInteger(); //随机对象 private static Random r = new Random(); public Provider(BlockingQueue queue){ this.queue = queue; } @Override public void run() { while(isRunning){ //随机休眠0-1000毫秒 表示获取数据 try { Thread.sleep(r.nextInt(1000)); //获取的数据进行累计 int id = count.incrementAndGet(); //比如通过一个getData()方法获取了 Data data = new Data(Integer.toString(id),"数据"+id); System.out.println("当前线程:"+ Thread.currentThread().getName() + ",获取了数据,id为:"+ id+ ",进行装载到公共缓冲区中。。。"); if(!this.queue.offer(data,2,TimeUnit.SECONDS)){ System.out.print("提交缓冲区数据失败"); } } catch (InterruptedException e) { e.printStackTrace(); } System.out.print("aaa"); } } public void stop(){ this.isRunning = false; } }
public class Consumer implements Runnable { private BlockingQueue<Data> queue; public Consumer(BlockingQueue queu){ this.queue = queu; } //随机对象 private static Random r = new Random(); @Override public void run() { while(true){ try{ //获取数据 Data data = this.queue.take(); //进行数据处理,休眠 0-1000毫秒模拟耗时 Thread.sleep(r.nextInt(1000)); System.out.print("当前消费线程"+Thread.currentThread().getName() +",消费成功,消费id为"+data.getId()); }catch(InterruptedException e){ e.printStackTrace(); } } } }
public class Main { public static void main(String[] args){ //内存缓冲区 BlockingQueue<Data> queue = new LinkedBlockingQueue<Data>(10); //生产者 Provider p1 = new Provider(queue); Provider p2 = new Provider(queue); Provider p3 = new Provider(queue); Consumer c1 = new Consumer(queue); Consumer c2 = new Consumer(queue); Consumer c3 = new Consumer(queue); //创建线程池,这是一个缓存的线程池,可以创建无穷大的线程,没有任务的时候不创建线程,空闲线程存活的时间为60s。 ExecutorService cachepool = Executors.newCachedThreadPool(); cachepool.execute(p1); cachepool.execute(p2); cachepool.execute(p3); cachepool.execute(c1); cachepool.execute(c2); cachepool.execute(c3); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } p1.stop(); p2.stop(); p3.stop(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }