一、synchronized 的实现原理
如果在方法里面使用 synchronized(obj) 这种用法,那么 javac 在编译的时候会插入 monitorenter 和 monitorexit 字节码。规则如下:
monitorenter 指令是在编译后插入到同步代码开始的位置;而monitorexit是插入到方法结束处和异常处
每个monitorenter 必须要有对应的 monitorexit 配对
任何对象都有一个 monitor 与之关联
示意图如下所示:
如果在使用 synchronized doSomething() 这种方式的话,会在字节码上的 flag 上添加 ACC_SYNCHRONIZED 标记,不过底层原理还是 monitor 这套。示意图如下:
一般的 Java 对象头(除了对象体和数据外的部分)会包括 2 个部分,如果是数组对象就会新增数组长度:
MarkWord: GC 信息、 hashCode、锁信息 等
KlassPoint: 对象所属的类
ArrayLen(是我自己写的英语,不一定是这个): 数组长度
上述说的锁信息,包括锁状态、是否偏向、锁标志位等,当然,需要注意一点的是,对象头的锁信息不是一成不变的。如:偏向锁时,存放的是偏向线程的 id;轻量级锁的时候,存放了栈中锁记录的指针;重量级锁的时候,存放互斥量(重量级锁)的指针。
二、一线大厂的面试题
1、synchronized 修饰普通方法和静态方法有什么区别?
synchronized 锁一定是和某个对象相关联的,加锁要加在具体地对象上。普通方法修饰当前对象,静态修饰class 对象。思考下:同时存在被 synchronized 修饰的普通方法和 静态方法,这二者能不能同时执行?
1 | public synchronized void normalMethod(){ |
答案是肯定能够同时执行的,因为这二者压根锁的不是同一个对象,是2个不同的锁,所以肯定可以同时执行。
2、什么是可见性
这涉及到工作内存和主内存。多个线程共享一个变量,如果一个对象更改了这个共享值,其他线程也能立即看到修改值。那如何保证可见性呢?采用 volatile 和 加锁
3、锁的分类
根据不同的场景可以将锁分为多类,如下图所示:
3、CAS的原理
主要利用现代CPU提供的 比较并交换(CAS)指令,这个指令具有原子性。CAS 存在的问题:
ABA问题
开销问题
只能适应简单变量问题
4、ReentrantLock 实现原理
进入一次锁,state 就加 1 ,退出一次锁,就减 1 。当然,底层是采用 AQS 实现的
5、AQS 原理
可重入锁、CountDownLatch 、读写锁 等都是使用 AQS
如果需要自己定义一种锁,只需要创建静态内部类,继承 AQS ,这是模板类,只需要实现几个关键方法如 tryAcquire 即可
6、Synchronized 原理
Synchronized(obj) 的形式是基于 monitorenter 和 monitorexit 字段
修饰的是方法的话,在字节码中的Flag 字段中会添加 ACC_SYNCHRONIZED 标记
7、synchronized 和 ReentrantLock 区别
一个是显式锁,一个是关键字,内置所
synchronized 是非公平锁,ReentrantLock 可以公平和非公平
8、锁的优化
偏向锁
轻量级锁
锁消除(与后续的逃逸分析联系比较紧密)
锁粗化
逃逸分析
9、volatile 在 DCL上的作用
new 一个对象会有3个步骤:
分配内存
初始化空间
赋值给引用
但是会有指令重排序,上述的 3 个步骤不一定按顺序完成,如果分配内存、赋值引用执行了,但是初始化空间还没完成,这时候双重检查的单例方式可能会使用初始化一半的对象,使用就会报错。
所以需要 volatile 关键字修饰
10、如何退出一个线程
采用线程的 interrupt 方法,利用中断机制来退出线程。
11、线程池原理
为什么要用线程池:
降低资源消耗
提高响应速度
提升线程的可管理性
再讲一讲线程池里面各个参数的含义。后续的线程拒绝策略,系统提供的4种拒绝策略
12、t1、t2、t3 三个线程,如何保证按顺序执行
采用 join 方法嘛
三、了解synchronized升级过程
通过CAS操作来加锁和解锁。采用自旋锁方式获取锁,万一其他线程很快释放锁呢。避免阻塞。
oracle 发现在很多情况下,1个锁在很多情况下都是同一个线程来获取的,所以这时候 CAS 操作都不想做了,这时候就产生了 偏向锁。(不过markword的字段更改的时候还是得用CAS操作的)
synchronized 整个的升级过程如下图所示:
偏向锁和轻量级锁的对象头是完全不一样的,所以在撤销偏向锁改为轻量级锁的时候,需要替换 markword 。
但是撤销偏向锁的时候,会 STW ,撤销偏向锁的时候,需要修改线程 1 的堆栈内容的。markword 会存放在 线程 1 的堆栈里面,线程 2 要跨线程把线程1的堆栈的数据改过来。所以需要 Stop The World