学习自寒食君 www.bilibili.com/video/BV1xT…
和blog.csdn.net/qicha3705/a… (牛逼)
什么是锁?
这个都会吧
JVM 运行时内存结构
红色部分是线程私有的,也是安全的,蓝色部分则是线程共享的,java堆存放对象,方法区存放类信息,常量,变量等,需要人工同步
Java 对象头结构
每一个object对象都有一把锁,锁信息是存放在对象头信息中的,记录当前对象被哪个线程占有
对象包括三部分:对象头、实例数据、填充字节
填充字节
他是为了满足Java 原则——每个对象都必须是8bit的倍数,填充的无用字节
对象大小=8 byte * n(hotSpot VM)
对象大小=8 bit * n( JVM)
实例数据
初始化对象时设置的属性、状态等信息
对象头(本次重点
-
他存放了一些对象本身运行的信息
-
包括Mark Word和Class Point
-
它属于是对象额外的存储开销,因此被设计的极小来提高效率
-
Class Point是一个指针,他指向了对象在 方法区 对应的 类元数据 的内存地址 (8字节 64位系统)
-
Mark Word 存储了很多和当前对象运行时相关的数据信息,重中之重 (8字节 64位系统)
synchronized
synchronized 经过编译后会生成两个字节码指令 monitorenter和monitorexit
写个demo验证下
public class TestSynchronized {
private int num = 0;
public void test(){
for (int i = 0;i < 1000;i ++){
synchronized (this){
System.out.println("thread: " + Thread.currentThread().getId() + " ,num:" + num);
num ++;
}
}
}
}
javac TestSynchronized.java 编译为class文件
javap -c TestSynchronized.class 反编译
synchronized 的锁机制。
简单了解下monitor?
它被叫做管程或者监视器,可以理解一个monitor就是一个房间,很多线程相当于很多客人,只有一个线程进入后离开,下一个才能进入
-
1.Entry Set中有很多想要进入monitor的线程,他们正处于Waiting状态
-
2.假如线程A成功进入monitor,那么他就处于Active状态,假设此时A的逻辑需要暂时让出执行权,那么他会进入Wait set,状态变为Waiting,
-
3.此时Entry set中的其他线程就有机会进入monitor,假设线程B进入,并且完成任务,线程B可以通过notify的形式唤醒Wait set中线程A(不一定是A),让A继续进入monitor来完成任务
-
执行完后,A、B exit退出
synchronized 的缺陷
synchronized可能存在性能问题?(我记得jvm优化了?)
-
synchronized 依赖 monitor ,monitor 依赖 操作系统的 mutex lock 实现的
-
Java 线程实际上是对操作系统线程的映射,所以每当挂起或者唤醒一个线程,都要切换操作系统的内核态,这种操作比较重量级,有些时候,切换线程的时间会超过程序执行时间,这样使用synchronized会对服务性能有很大影响。
-
从java 6开始,对synchronized进行了优化,引入了偏向锁、轻量级锁,
-
所以从重量级从低到高:无锁,偏向锁、轻量级锁,重量级锁,这就对应了Mark Word中的四种状态,
-
锁只能升级,不能降级
锁的四种状态
无锁
-
情况1:无竞争
-
情况2:存在竞争,非锁方式同步信息,比如说某一个对象,我同一时间只允许一个线程修改成功,其他的线程不断循环重试,这就是CAS
偏向锁
-
首先是为何需要偏向锁?
-
如果只有1个线程竞争对象锁,那么我们最理想的方式,就是不通过线程切换,也并不用通过cas来获取锁,因为CAS也需要耗费一些资源,最好对象锁能认识这个线程,只要这个线程过来,对象就把锁交出去。
-
如何实现偏向锁?
-
在Mark Word中,当锁标志位是否是01,如果是判断倒数第三位是否是1,如果是,就是偏向锁,再去读Mark Word前23个bit,也就是线程id,如果是,直接进入
-
如果不是申请锁的id,也就是对象发现不止有一个线程,而是有多个线程想竞争锁时,那么会升级为轻量级锁
轻量级锁
-
如何实现轻量级锁?
-
将Mark Word前30个bit 记录指向栈中锁记录的指针
-
- 当一个线程想要竞争轻量级锁时,假如看到锁标志位为00,就知道他是轻量级锁,这时线程会在自己的JVM虚拟机栈中开辟一块空间(Lock Record,他存放的是对象头中Mark Word副本 以及owner指针)
-
线程通过cas去尝试获取锁,一旦获得,就会复制该对象头Mark Word 到自己的Lock Record中,并且将自己Lock Record中的owner指针指向该对象,
-
另一方面,该对象中的Mark Word前30个bit将会生成一个指针,指向线程虚拟机栈中的Lock Record,这样就实现了对象和线程的绑定,他们就互相知道了对方的存在;
-
这时,该对象已经被线程锁定了,获取了这个对象的线程就可以执行任务了,此时其他线程将会自旋等待(先适应性自旋,尝试获取锁。到达临界值后,阻塞该线程,直到被唤醒),其他线程一直循环查看该对象的锁有没有释放(也就是该对象头的Mark Word前30个bit?),
-
适应性自旋
-
自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。
如何查看对象头信息?
<!--查看对象头工具-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
public static void main(String[] args) {
Object dog = new Object();
System.out.println("初始信息:"+ ClassLayout.parseInstance(dog).toPrintable());
System.out.println(dog.hashCode());
System.out.println("hashcode信息:"+ClassLayout.parseInstance(dog).toPrintable());
synchronized (dog) {
System.out.println("加锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
}
System.out.println("释放锁后信息:"+ClassLayout.parseInstance(dog).toPrintable());
}