前言:
这几天,请教了我的大神师兄,他和我说了在大厂面试的经过,四面面到他怀疑人生,最终他拿到了 sp 级别的 offer,很感谢师兄他把面试中各种知识点向我分享。接下来,笔者会针对该次面试所要求的知识点进行学习强化,并写成博客,与大家分享。师兄所在学校是三本(名气也一般般),和他同期面试的最低都是 985 211 好像还有硕士级别的。个人认为,有实力才是真的牛逼,学历只是敲门砖,没有必要因为自己的学历低而没自信。要学习师兄那种,同台竞争,展示自己的实力即可,粗暴一些说:教他们怎么做人(开玩笑哈,只是告诉大家选好方向,努力就好)。
接下来是二面的部分知识点: 也是面试里面最简单的部分了 —— java 内存模型。
java 内存模型:
java 内存模型的主要目标是定义程序中的各个变量的访问规则,包括静态变量,数组对象,以及实例变量。不包括为局部变量(方法参数也属于局部变量),因为局部变量是线程私有,不存在竞争关系,所以不用理会。
如下图,我们代码所有的变量都是存在主内存中的。此外每条线程都有自己的工作内存,工作内存保存了线程所需要使用到的主内存变量的副本拷贝,线程对变量的所有操作都必须在工作内存中完成,而不是直接去操作主内存。在一条线程执行时,该线程是无法直接访问其他线程的工作内存中的变量,只有当该线程将方法内指令(代码)全部执行完后,回写到主内存中时,其他线程开可见。
小结:总体来看,线程与线程间的通信必须经过主内存才可以进行交互。这里知道局部变量是线程私有等就可以了。下面会介绍内存模型的三个特性,请往下看。
java 内存模型的三个特性:
- 可见性:
可见性是指当一个线程修改了共享变量的值时,其他线程能够立刻得到这个修改。
- 原子性:
由 java 内存模型来直接保证的原子性变量操作。
简单些可以用事务的方式来理解:一个操作不能在执行过程中不能被打断,要么全部完成,要么全部不完成,失败时回滚到操作之前的状态。
- 有序性:
在程序执行过程中,普通变量的赋值操作可能和我们写的代码顺序可能是不一致的,比如:代码变量的赋值操作在第四行,在执行过程可能第一个执行的代码就是赋值。即顺序可能不一致,但是虚拟机会保证变量在该方法执行过程中都能获得正确的结果。
java 中的关键字
volatitle 关键字
volatitle 关键字对于开发人员来说可能比较陌生,但是如果有看过 DCL 的同学应该有见过这个关键字。在上面三个特性中该关键字得到了 2 个特性。volatile 关键字可以保证可见性有程序执行的有序性,如何理解?首先,我们在写完代码后正常的思路确实是程序的执行是自上而下的,这个属于先行发生的原则,但是在线程内是串行,但是在其他线程看来,是没有排序,可能是乱的,也就是不一定执行按照我们书写的逻辑一样自上而下执行。即使如此,虚拟机依旧会保证返回结果的正确性。
代码示例:
public class Test {
private int a; //普通变量
private volatile boolean b =false ; //volatile修饰的变量
public int getA() //获取变量值
{
return this.a;
}
public void setA(int a) { //设置变量值
this.a = a;
}
public boolean isB() {
return b;
}
public void setB(boolean b) {
this.b = b;
}
/**
* 这代码是没有意义的,仅供理解
*/
public int testMethod(){
int j = 10;
int i =j;
a=j;
//这里有一层屏障
b = true;
//这里有一层屏障
j=a;
i=j;
return a;
}
}
- volatile 的有序性:
上面讲到,volatile 关键字是有有有序性的规则的,后面又说执行是乱序的,那 volatile 关键字是如何保证有序性的。看上面的代码,我们的 volatile 修饰的是中间那段代码,在该关键字前面的代码不会跑到 volatitle 关键字后面去执行,但是,在该关键字前面的代码依旧可以乱序执行。关键字后面的代码也类似,具体理解往下看即可。
JMM 的内存屏障插入策略:
① 在 volatile 写操作前后各加一层内存屏障,
② 在 volatile 读操作后加入 2 层内存屏障。
如下图:
volatile 写操作的屏障
volatile 读操作的屏障
- volatile 的可见性:
根据上面的学习,我们知道普通变量是没办法保证修改后立即同步到主内存中的。而 volatitle 修饰的变量又是如何保证可见性的呢?
我们先看看普通变量与 volatitle 变量的程序执行图。
普通变量:
volatitle 变量:
注意:前面提到线程有自己的工作内存,从主内存中拷贝来需要使用的变量副本,变量副本的操作都是在工作内存中完成的,最后才是回写到主内存中。
而 volatile 关键字修饰的变量会将本地内存无效化,强制需要去主内存中拿,保证了变量每次取到都是最新。这样就保证了 volatile 变量的可见性。
volatile 变量存在弊端:
volatile 在上面讲了可以保证可见性与有序性,但是有一点要注意 volatile 并不能保证原子性。举个例子:java 中的运算,即使是 volatile 变量,也依旧会造成数据不一致(原因:java 的运算并非是原子性操作,导致 volatile 变量的运算在并发下出现不安全现象)
如下代码:
public class VolatitleTest {
public static volatile int rece = 0;
public static void increase(){
rece++;
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[20];
for (int i=0 ;i<20 ; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
//循环累加 1000 共进行 20 次
for (int j = 0 ;j<1000;j++) {
increase();
}
}
});
threads[i].start();
}
Thread.sleep(10000);
System.out.println("得到的值是:"+rece);
}
}
运行结果如下图:
按道理,我们得到的值应该是 20000 才对,为什么少了?
我们知道变量 ++ 是如下操作:
(1) 获取变量值
(2) 将获取变量 +1
(3)写回主内存
在第一步时,我们获取的变量都是最新的,没问题,但是第二步骤,第三步骤都不是原子呀。举个例子:线程 A 和线程 B 都拿到了 rece = 0,A 和 B 分别在 CPU 中自加,最后写回主内存,好像没错,可是两个线程都是同一个值写回主内存,这时就总值就变少了。
synchronized 关键字
这个关键字就相信大家很常见了,也正是因为简单常见,容易被程序员滥用。这篇先不解释该关键字,准备用一片新博文来整理 java 中的各种锁。
final 关键字
final 关键字比较简单,final 关键字的可见性是指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 “this” 的引用传递出去(this 逃逸),那么其他线程可以立即看到 final 的字段值。
先行发生原则:
java 内存模型下有一些先行发生的关系,这些先行发生关系不需要任何同步器(锁之类的)就已经存在的,一般来说存已某些关系的,并且可以推倒出来的就会发生先行发生规则。
可以发生先行发生规则的:
① 程序次序规则:在一个线程内,可以理解为就是我们编写的代码逻辑,也就是我们书写的代码顺序。
② volatile 变量原则:volatitle 写操作先行发生与这个变量的读操作
③ 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生与操作 C ,可以得出 A 先行与 C。这里就像我们的公式: a= b,b=c,所以得出 a=c。
④ 锁规则:unlock 操作先行与 lock 操作,且必须是同一把锁。
剩下的就是一些线程的规则了,这里不描述。
注意:时间先后顺序和先行发生原则是基本没有太大的关系,平时开发注重先行发生原则即可,不用太过于去考虑时间的先后顺序。
总结:
这篇博客写的时间有点长了,但是还是收获满满,以为看了很多遍书,应该可以写出来。结果在下笔的时候老是发现不知道如何表达比较好,考虑举例子也是个心累的事。加油,干就对了,努力一把,准备拼一把秋招。祝大家学习进步,工作顺利,熬过这个互联网寒冬期。最后,感谢大家的阅读。
程序人生,与君共勉~!
近期刚刚开始在做公众号:公众号的定位是全方面,包括投资理财,技术,还有可能会分享一些设计作品。如果有兴趣的朋友欢迎关注公众号。