本篇为根据马士兵老师公开课所讲内容记录的笔记。十分感谢马老师精彩讲解。
视频地址: 马士兵老师公开课
用户态、内核态
以前的操作系统,os(操作系统)和app(用户应用程序)是在同一级别。这就导致app可以修改操作系统所占用的内存等,导致系统崩溃。
现在的操作系统,内核与硬件(内存、网卡等)打交道,所有的app运行在内核的外部。app需要与内核交互才能获取相应的资源。
(蓝色矩形代表内存)
java多线程模型
- JVM虚拟机规范没有要求。Hostpot(Oracle的JVM实现) 1:1。
- JVM每个线程对应OS中一个线程。
- 线程锁、线程调度都交给OS来处理
- 重量级线程,OS管理线程
- go 语言 纤程M:N
- 轻量级线程,不需要OS管理线程。自己管理
- M:N 具体实现是GPM模型。M个分成不同的队列,OS里有N个实际线程。N个实际线程去队列里不断的取任务
进程与线程区别
进程:分配资源的基本单位。给进程分配内存空间、端口、文件句柄等。
线程:执行操作的基本单位。进程运行起来是从main线程开始,实际上是不同的线程。他们占用的都是进程分配的那些资源。
线程不是越多越好,因为线程切换时cpu要进行保存线程和恢复,切换过多反而会浪费时间。
线程的数量往往有压力测试来决定。
锁
Syncnaized从重量级锁升级到轻量级锁(JDK1.2)
AtomicInteger 原子类实现:
最终调用compareAndSwapInt()实现,调用的C++
底层:CAS
compare and swap/compare and exchange
在往回写的时候,如果原数没变,说明没人改,就写回去。
如果有人改,就重新读出来重新加。直到成功
这就是自旋锁/乐观锁
问题:
- A->B->A问题:例如,A线程改之前m=1,中间m被其他线程修改成2,然后又改成了1,这时A线程在回写的时候,并不能发现m被人改过。解决方案:给m加版本号。
- compare and set过程是否具备原子性? 初始m=1
- 如果线程A顺利回写m=2,则两步:1.m没被改变,仍然为1, 2. 给m新值。
- 如果在1,2步之间,另一个线程读取了m,并写回了新值m=8, 那么线程A回写会将m=8覆盖掉。
- 所以比较和赋值必须是原子性,不能被打断。
- 具体实现在c++,利用了CPU的指令,用汇编嵌入写的。cmpxchgl,底层支持。
简单同步,优先使用synchrasied,因为现在已经升级了CAS。
面试重点
- markword 64位jvm,8字节。
- 指向具体.class地址,getClass()就是根据这个指针拿的。
- 类包含的实例数据
- padding 对其为了让他能被8字节整除。装配效率高。
org.openjdk-jol 能够打印类的内存布局信息。
System.out.println(ClassLayout.parseInstance(o).toPrintable());
一个new Object在内存中占用16个字节。
markword信息:
- 锁信息
- hashcode
- gc信息
java内部实现一般小端
锁升级过程
偏向锁,严格讲不是一把锁,只是在markword里记录了线程指针。
自旋锁,竞争锁。谁抢到是谁的,没有等待队列,消耗cpu。通过CAS操作。谁填进去自己的ID号就是谁的。不需要OS调度。
重量级,有等待序列,需要CPU调度。但是不消耗cpu资源。调用wait或者notify的一定是重量锁。
重量所一定比自旋锁效率高吗?
不一定。自旋锁如果有个在里面时间很长,其他一直在外面争抢。
1.6之后自动适应,什么时候升级成重量级JVM控制。
偏向锁有一个使用时延。新建一个普通对象时,上来是自旋锁,然后4s之后如果再变成偏向锁。
Volatile
- cpu的缓存行,一般是64字节。每次都换一行。为了追求极致性能,可以在两个变量中间加入其他的无用数据,强制让这两个变量不在一个缓存行,那么在多线程同时修改这两个数据时,volatile同步性能会有较大的提升。因为不用频繁通知更新这个变量了。(要用static修饰。)
指令重排序
指令重排序:cpu运行的顺序可能与自己写的代码顺序不一样,这种现象叫做乱序执行。当编译器认为两行没有关系的时候,可能会改变顺序来提升效率。
可以发生指令重排序的:JVM 规定8种可以。 搜索关键词: happens before
volatile能够禁止指令重排序的原因:
在两个指令之间添加一个屏障: memory barriar
JVM级别的内存屏障:四种
Volatile实现细节:
-
JVM规范的要求:
在对volatile声明的内存写前后都加入屏障,读之后加入屏障
-
底层硬件的实现:
hostpot实现,在x86平台,使用的是cpu的lock指令,
该指令对共享的内存单独使用。能够将当前cpu用的缓存写到内存,令其他的cpu缓存失效。还提供了有序指令无法越过内存屏障的作用(锁总线了)
线程池面试题
多线程发起多个任务,如果所有校验结果为true,就返回true,只要有一个返回false,就返回false。尽量快。请完成isAllTrue()
分析: 只要有一个返回false了,就直接返回结果。不能等待全结束再返回,除非全是True。取消掉其他线程。
Future-> Future.get()是阻塞的。
guava -> ListenableFuture,不阻塞,直接返回。JDK1.8出了个CompletableFuture。