并发编程相关面试题一
一、volatile
1、volatile的应用
在多线程并发程序中synchronized和volatile都扮演者着很重要的角色,volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的可见性,能够防止脏读,被volatile关键字修饰的变量,如果值发生了改变,其他线程立刻可见;
可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值,如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低;
2、volatile定义
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量;java语言提供了volatile,在某些情况下比锁更加方便,如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的;
3、volatile和synchronized有什么区别
volatile能够保证数据可见性,但是无法保证数据的原子性;
synchronized能够保证数据可见性,也能保证数据原子性;
4、volatile使用条件
只能在有限的一些情形下使用volatile变量替代锁;要使volatile变量提供理想的线程安全,必须满足下面两个条件:
①对变量的写操作不依赖于当前值;
②该变量没有包含在具体变量的不变式中;
实际上,这些条件声明,可以被写入volatile变量的这些有效值独立于任何程序的状态,包含变量的当前状态;
第一个条件的限制使volatile变量不能用作线程安全计数器;虽然增量操作(i++)看上去类似于一个单独的操作,实际上它是一个由(读取-修改-写入)操作序列组成的组合操作;必须以原子方式执行,而volatile不能提供必须的原子特性;实现正确的操作需要使i的值在操作期间保持不变,而volatile变量无法实现这点;
5、volatile优点
①内存中只有一个对象,减少内存开销;
②单例可避免对资源的多重占用,例如写文件工作,可避免对同一资源文件的同时写操作;
6、volatile缺点
①单例模式一般没有接口,扩展很困难;
②不利于测试,并行开发时,若单例未完成,则不能进行测试;
③与单一职责原则冲突;
二、指令重排序
1、什么是指令重排序
java语言规范JVM线程内部维持顺序化语义,即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码逻辑不一致,这个过程就叫做执行重排序;
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,重排序分为三种类型:
①编译器优化的重排序:编译器在不改变单线程程序的语义下,可以重新安排语句的执行顺序;
②指令级并行重排序:线程处理器采用了指令级并行技术来将多条指令重叠执行;如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;
③内存系统重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱执行;
最终执行的指令序列示意图:
上述的1属于编译器重排序,2和3属于处理器重排序;这些重排序可能会导致多线程程序出现内存可见性问题;对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序;对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型处理器重排序;
2、内存屏障
为了保证可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序;
内存屏障类型表:
StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他三个屏障的效果;
三、先行发生原则Happens-before
从JDK1.5,java使用新的JSR-133内存模型;JSR-133使用happens-before的概念来阐述操作之间的内存可见性;
在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么者两个操作之间必须要存在happens-before关系;这里两个操作可以是在一个线程之内,也可以是在不同线程之间;
happens-before八大原则
1.程序次序原则:
在一个线程内,按照代码的顺序,书写在前面的代码优先于书写后面的代码;
2.管程锁定规则:
一个unlock操作先行发生于后面对同一个锁的lock操作,注意是同一个锁;
3.volatile原则:
对于一个volatile变量的写操作先行发生于后面对变量的读操作;
4.线程启动原则:
Thread对象的start()方法优先于此线程的每一个动作;
5.线程终止原则:
线程中所有的操作都优先发生于此线程的每一个动作;
6.对象中断原则:
对象的interrupt()方法的调用优先发生于被中断线程的代码监测中断事件的发生;先中断再检测;
7.对象终结原则:
一个对象的初始化(构造函数执行完毕)完成优先发生于它的finalize()方法的开始;
8.传递性
如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
四、线程安全的三要素
1、原子性(Atomicity)
原子,一个不可再被分割的颗粒。原子性,指的是一个或多个不能再被分割的操作。
int i = 1; // 原子操作 i++; // 非原子操作,从主内存读取 i 到线程工作内存,进行 +1,再把 i 写到主内存。
虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为“复合操作”。
我们可以用synchronized 或 Lock 来把这个复合操作“变成”原子操作。
2、可见性(Visibility)
Java就是利用volatile来提供可见性的。
当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。
其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
3、有序性(Ordering)
lock/unlock, volatile关键字可以产生内存屏障,防止指令重排序时越过
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。比如下面的程序段:
double pi = 3.14; //A double r = 1; //B double s= pi * r * r; //C
上面的语句,可以按照A->B->C
执行,结果为3.14,但是也可以按照B->A->C
的顺序执行,因为A、B是两句独立的语句,而C则依赖于A、B,所以A、B可以重排序,但是C却不能排到A、B的前面。JMM保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。
如图所示,write方法里的1和2做了重排序,线程1先对flag赋值为true,随后执行到线程2,ret直接计算出结果,再到线程1,这时候a才赋值为2,很明显迟了一步。
这时候可以为flag加上volatile关键字,禁止重排序,可以确保程序的“有序性”,也可以上重量级的synchronized和Lock来保证有序性,它们能保证那一块区域里的代码都是一次性执行完毕的。