面试是类似演讲演说的过程,网上的面试资料都是文字的,没办法让面试者直观的体验到面试过程,希望这个音频节目对你有帮助。
纯粹硬核IT知识,技术小白请绕路。
搜集整理各种面试官提出的技术问题。
为IT人提供专业知识的面试指导。
... more
说到多线程的原子性、可见性和有序性。
这是多线程确保线程安全的三个标准。
首先。咱说说。原子性。原子性其实很好理解。原子就是最小的单元,他就是可执行的最小的单元。在程序执行的时候,最小的一个可执行单元就是一个原子。
一段原子性的代码执行的时候。不会被打断。这一段代码的执行,要么不执行,要么全部执行完毕。
这段代码也许只有一行代码,也许是多行代码。
一行代码很多也不是原子性的,因为这个原子性并非是我们Java程序代码的原子性,而是CPU执行 阶段的原子性。并不是说代码少他就是原子性。也不是说代码多的就不是原子性。一行代码很多都不是原子性的,多行代码加上锁,也可以是原子性的。
比如。定义一个整型int i = 10。这一行代码它就是原子性的,这里是给整形变量i赋值,这个就是原子性的,它不可能被打断。
如果定义的一个长整型的 long j=10,这就不是原子性的,为什么呢?因为这个long类型啊,他是64位的,64位的长整型。在CPU中执行赋值操作的时候,它是分两步,分别64位的高位和低位进行赋值,这是分两步来完成的。类似的double也是这样子,double也是64位的,会分两步分别给高32位和低32位赋值,就是两步操作,不是原子性的。
那么咱们来看另一个语句。int i=10; i++ ; 这个i=10我们刚刚说过了,是原子性的,那这个 i++是不是原子型的呢?直接说答案:i++不是原子型的。
别看这么简单的一个计算,写程序就一行,但到了CPU级别就需要3条指令,1 获取i的值, 2 执行i+1 的操作,3 把i+1的结果赋值给i 。所以这个i++不是原子性的。
这么简单的语句都不是原子性的,那么是不是对于多行代码就肯定不是原子性了呢?其实也不是,Java可以利用锁来保证多行语句是原子性的。
从CPU的角度来看,就是插入一个Lock指令。当一段代码被Lock指令锁住后,一个线程执行这段代码的时候,其他线程就无法执行,只能等着这个线程执行完毕才能获得执行权。这就是保证这段代码的原子性。
具体在Java代码里面写的时候就用synchronized 和lock对象,来实现一段代码,一个方法,一个对象的锁。这是对原子性的解释。
那么可见性是什么呢?在Java的内存模型中,每一个子线程会拥有一个单独的工作内存,主线程有一个主内存。主内存中存放的是共享变量,虽说是共享变量,看起来是被主线程和子线程共享的,那么子线程就可以读写操作该共享变量了吧?其实不行的,子线程是不能直接读写操作主内存中的共享变量的。
子线程会在工作内存中创建该共享变量的副本,然后读写操作该副本。这里再强调一下,子线程不能直接读写主内存的共享变量,只能读写工作内存中的共享变量的副本,读写完毕后再将副本的值回写到主内存中。
这时候,我们就发现一个问题了,假设主线程中有共享变量 x = 10 , 有两个子线程 A和B,要操作共享变量x,就会分别在各自的工作内存中加载一份x的副本。这时候咱们看到了,原来只有一个x,现在变成了3份:主内存中的x和两个副本x。
如果子线程A对副本x做了+1 操作,线程A中的x就变成 11 了,但线程B看到的x还是10,这就出现了可见性的问题。
通过上面的描述,总结一下什么是线程的可见性问题:就是多线程情况下,一个线程修改了变量的值,另一个线程看不到这个变化。
那怎么解决这个问题呢?思路是这样的,对于线程A来说,只要修改了x副本的值,马上把这个值回写到主内存中;对于线程B来说,只要想读x的值,就再次从主线程加载x,这样副本的值永远都与主内存中的值是一样的。这就可以解决可见性问题了。
在具体实现上Java内存模型是通过内存屏障来实现的。
那这里又引出一个内存屏障的概念,啥是内存屏障呢?内存屏障其实就是一个指令,将这个指令插入到其他指令之间,会执行某些特殊的操作。具体到解决可见性是两个内存屏障指令:load屏障和store屏障。 load和store这俩词儿之前讲内存模型的8个指令的时候见过,load指令是工作内存从主内存加载共享变量生成副本的指令,store指令是将工作内存的共享变量副本回写到主内存的指令。那么load屏障就呼之欲出了,这是在指令A前插入load屏障,那么指令A用到的变量副本将失效,必须从主内存加载对应的共享变量的值并替换当前副本;对应的store屏障插入到指令B之后,那么指令B修改的副本的值,马上会被回写到主内存。
这个效果应该可以想象了吧?通过load屏障和store屏障,可以实现修改了副本马上回写到主内存,读副本之前先从主内存加载一下,这样就确保了,只要副本值修改了马上就更新到主内存里面,然后另一个线程读副本的时候直接装载主内存中的新值,这样就解决了多线程下变量的可见性。
接下来讲解一下有序性。
什么是有序性呢?这里的序是指什么的顺序呢?
这里说的序是指CPU中指令执行的顺序,就是一条条的汇编指令的先后顺序。我们Java程序员写的是Java代码,写出来一行行的Java代码最后编译转为汇编在CPU中执行,这时候就是一条条的汇编指令了。一般程序员的理解是这样的,写了两行代码,行A在行B的前面,应该是先执行A再执行B,但其实CPU在指令执行的时候做了优化,如果指令A和指令B调换顺序后不影响执行结果,那么CPU会进行指令重排,重排后就是先执行指令B再执行指令A。这个原则叫做as if serial ,就是好像是连续的一样,也就是重排序优化后就好像没有重排序,而是正常连续执行的那样。
比如:
int i=10;
int j=20;
这样的两条指令,先执行哪条其实是无所谓的,这时候CPU就会执行指令重排。
对于单线程程序来说,CPU指令重排绝对不会有问题,但对于多线程指令重排就有可能产生数据安全问题。
打个比喻:
刘能和赵四去买冰糕,这个指令顺序是,推门进冷饮店,掏钱,拿冰糕。
如果就刘能自己去买冰糕的话,先掏钱再拿冰糕还是先拿冰糕再掏钱,没什么关系,冷饮店老板照顾的过来。
现在刘能和赵四俩人一起去,结果赵四掏钱了,还没拿冰糕,刘能先执行了拿冰糕,这下赵四不干了,我掏的钱,我掏的钱。老板说,没办法,人太多忙不过来,先后顺序调整一下,这样快,你别叫唤了。
那赵四能干嘛?赵四大叫:不行啊,出现多线程的有序性问题了。
那咋解决这个问题呢?还是下内存屏障。
有序内存屏障是在两条指令之间插入的屏障,插入后,前后两条指令就不能重排序了。
处理有序性的屏障有4个。
LoadLoad屏障:下在load和load之间,这个屏障指令之前的load指令和之后的load相关的指令不能重排序;
LoadStore屏障:下在load和store之间,这个屏障指令之前的load指令和之后的store指令不能重排序;
StoreLoad屏障:下在store和load之间,这个屏障指令之前的store指令和之后的load指令不能重排序;
StoreStore屏障:下载store和store之间,这个屏障指令之前的store指令和之后的store指令不能重排序。
说白了,这个有序性的内存屏障,就是禁止CPU对前后的指令进行重排序的,有了这个屏障,多线程下CPU也不能做重排序优化了,就解决了有序性的问题了。
关于内存屏障的细节,我们今儿就不聊了,以后我们再细说。
这个多线程的原子性、可见性和有序性是啥意思,你明白了吧?
1 Java中有4中引用,强引用、软引用、弱引用、虚引用。
强引用:普通new出来一个对象 ,都是强引用。
软引用:SoftReference类,当内存不足的时候,会被回收。
弱引用:WeakReference类,内存充足也会被垃圾回收。
虚引用:最脆弱的引用,记录一个对象已经被回收了。
2 当某个对象只被弱引用的时候,Java的垃圾回收机制就会回收该对象。
3 我们知道一个对象定义出来,在堆内存中为其开辟空间,在栈中存储该对象的引用。比如定义一个Car a1 = new Car() ; 这时候在堆中开辟了一块空间存储Car的数据,而a1则存储在栈中,引用堆中的Car。
4 如果我们设置 a1 = null,那么堆中的Car空间就不被栈引用了,Java的垃圾回收就会回收这块内存。
5 如果我们设置 a1 = new Car() ; a2 = a1 ; a1 = null ; 这时候虽然 a1 不引用堆中的Car,但a2 还引用着堆中的Car,那么堆中的Car空间是不会被回收的。
6 这时候就可以使用弱引用了。 a1 = new Car() ; a2 = new WeakReference(a1) ; a1 = null。
这时候 a1 不再引用堆中的Car空间,a2 是一个弱引用,虽然引用着堆中的Car空间,由于堆中的Car空间只被弱引用,因此Java的垃圾回收就会回收该空间。
1 准备Spring的上下文环境,也就是ApplicationContext
2 扫描XML文件,或者是注解,得到一系列的BeanDefinitaion
3 BeanFactoryPostProcessor,Bean工厂的后置处理器, 要对BeanDefinition做一些处理,替换一些属性的值,比如MyBatis的Bean,就会做这个处理。
4 开始实例化Bean,即new出来Java对象,当然,他不是真的new,而是通过反射实例化Bean
5 开始初始化Bean,对Bean的属性赋值,即根据Bean的依赖关键,进行Bean的依赖注入
6 对Bean的后置处理,调用BeanPostProcessor,进行AOP相关的操作,这时候就会对Bean进行切面增强。
7 将Bean放入Bean容器,即一个HashMap中,这时候开发者就可以使用这个Bean了。
8 Bean的销毁,当Spring的AppliationContext要关闭的时候,会调用DiposibleBean的destory方法,或者Bean的destory方法,进行Bean销毁。
The podcast currently has 43 episodes available.
49 Listeners
367 Listeners
643 Listeners
229 Listeners
103 Listeners