上下文切换
即使是单核处理器也支持多线程执行代码执行代码,CPU通过给每个线程分配CPU时间来实现这个机制。CPU不停地切换线程执行,让我们感觉到多个线程是同时执行的。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
一个生动的例子:
当我们在读一本英语的技术书时,发现某个单词不认识,于是便打开英文字典,但是放下英文技术书之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
synchronized
synchronized一直是元老级角色,很多人都会称呼它为重量级锁。Java 1.6中为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁。
synchronized用的锁存在Java对象头中。
Java对象头的存储结构如下:
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁消息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数组的长度(如果当前对象是数组) |
Mark Word的存储结构如下:
锁状态 | 内容 | 锁标志位 |
---|---|---|
无锁状态 | hashCode | 对象分代年龄 | 偏向锁标志位0 | 01 |
偏向锁 | 线程ID | Epoch | 对象分代年龄 | 偏向锁标志位1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 |
重量级锁 | 指向互斥量(重量级锁)的指针 | 01 |
锁的升级与对比
锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
偏向锁
偏向锁加锁:
- 测试线程ID是否指向当前线程,如果是,进入步骤4,否则进入步骤2。
- 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行4;如果竞争失败,执行3。
- 如果CAS获取偏向锁失败,则表示有竞争,撤销偏向锁。
- 执行同步代码。
偏向锁撤销:
只有当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点。
它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果不处于活动状态,则将对象头设置为无锁状态。
如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么偏向于其他线程,要么恢复到无锁或者轻量级锁状态。
轻量级锁
轻量级锁加锁:
- 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
- 拷贝对象头中的Mark Word复制到锁记录中。
- 拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果更新成功,则执行步骤4,否则执行步骤5
- 如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”。
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
轻量级锁解锁:
会使用CAS将Displaced Mark Word替换回对象头,如果成功,表示没有竞争,如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。
参考
- Java并发编程的艺术[书籍]
- java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁