面试问题

-----2019-12-2

1、Volatile关键字的粗浅理解
在学习并发编程的时候了解到,volatile关键字有两个作用:

1. 并发环境可见性:volatile修饰后的变量能够保证该变量在线程间的可见性,线程进行数据的读写操作时将绕开工作内存(CPU缓存)而直接跟主内存进行数据交互,即线程进行读操作时直接从主内存中读取,写操作时直接将修改后端变量刷新到主内存中,这样就能保证其他线程访问到的数据是最新数据

2. 并发环境有序性:通过对volatile变量采取内存屏障(Memory barrier)的方式来防止编译重排序和CPU指令重排序,具体方式是通过在操作volatile变量的指令前后加入内存屏障,来实现happens-before关系,保证在多线程环境下的数据交互不会出现紊乱

Volatile无法保证原子性
由于volatile关键字的可见性,导致容易被误解其作用,以下描述是不准确的:“volatile变量对所有线程是立即可见的,对volatile变量所有的写操作都能立即反馈到其他线程中,volatile变量在各个线程中都是一致的,所以基于volatile变量的运算在并发下安全的”

使用volatile关键字虽然能够使线程共享的变量在并发情况下完全可见,起到线程信息交互和通信的作用,但对于非原子操作,volatile并不能保证该操作的原子性(即操作过程被其他线程干扰导致信息错误和信息丢失),最简单的例子就是i++这样的自增操作,以下是一个并发情况下的自增例子:

private static volatile int count = 0;

public static void main(String[] args){
Thread[] threads = new Thread[5];
for(int i = 0; i<5; i++){
threads[i] = new Thread(()->{
try{
for(int j = 0; j<10; j++){
System.out.println(++count);
Thread.sleep(500);
}
}catch (Exception e){
e.printStackTrace();
}
});
threads[i].start();
}
}
生成5条线程,每条线程都对count执行10次自增操作,按理说最后的结果应该是1到50均被打印出来,但不管运行多少次,都无法得到期望的结果,说明volatile标记的变量在并发环境下并不能保证线程安全。

诸如“i++”或者“++i”这样的操作并不是原子操作,因为自增操作包括三个基本指令:读取数据、计算数据、输出结果,可以看看i++相关的字节码:

Code:
0: getstatic #2 // Field count:I
3: iconst_1
4: iadd
5: putstatic #2 // Field count:I
8: return
getstatic指令将变量从主内存中读取出来,这时候如果该变量时volatile修饰的,那可以完全保证此时取到的是最新信息,但在iconst_1指令(入栈)和iadd指令(自增计算)执行过程中,该变量有可能正在被其他线程修改,最后计算出来的结果照样存在问题,因此volatile并不能保证非原子操作的原子性,仅在单次读或者单次写这样的原子操作中,volatile能够实现线程安全

volatile如何保持内存可见性

volatile的特殊规则就是:

  • read、load、use动作必须连续出现
  • assign、store、write动作必须连续出现

所以,使用volatile变量能够保证:

  • 每次读取前必须先从主内存刷新最新的值。
  • 每次写入后必须立即同步回主内存当中。

Volatile结合CAS实现原子性
2、CAS(CompareAndSwap)

比较交换原则结合volatile,就能够实现基本的线程安全,典型的应用就是concurrent包下的Atomic类,上述例子如果用AtomicInteger代替int,就能够实现自增情况下的线程安全

3、堆排序?

posted @ 2019-12-02 22:59  image_erfsfj  阅读(129)  评论(0编辑  收藏  举报