简介
在 Java SE 1.6 之前, Synchronized 被称为重量级锁. 在 SE 1.6 之后进行了各种优化, 就出现了偏向锁, 轻量锁, 目的是为了减少获得锁和释放锁带来的性能消耗.
Synchroized 的使用 (三种形式)
(1) 对于普通同步方法, 锁是当前实例对象. 如下代码示例:
解释: 对于 set 和 get 方法来说, 都是在方法上使用了同步关键字, 所以他们是同步方法, 锁的就是当前的实例对象, 怎么理解了, 看下面的 main 方法, 就是这个 new 的实例对象. 所以他们的锁对象都是 synchronizedMethod 这个实例.
- private int i = 0;
- public synchronized void setNum (int number) {
- this.i = number;
- }
- public synchronized int getNum () {
- return i;
- }
- public static void main (String[] args) {
- // 启动两个线程调用 get 和 set 方法
- SynchronizedMethod synchronizedMethod = new SynchronizedMethod();
- new Thread(() -> {
- synchronizedMethod.setNum(5);
- },"set").start();
- new Thread(() -> {
- int num = synchronizedMethod.getNum();
- System.out.println(num);
- },"get").start();
- }
(2) 对于静态同步方法, 锁是当前类的 Class 对象. 看代码示例:
解释: 如下两个方法都是静态同步方法. 所以锁是当前类的 class 对象, 这么理解吧, 静态方法是类调用的, 所以锁就是这个类对象. 如下代码运行结果, 只有当类的第一个静态同步方法执行完毕, 第二个才能执行.
- /**
- * synchronized 静态方法
- */
- public class SynchroizedStaticMethod {
- private static int i = 0;
- public static synchronized void addNum () {
- for (;;) {
- i++;
- System.out.println(Thread.currentThread().getName()+"----"+i);
- if(i>= 100){
- break;
- }
- }
- }
- public static synchronized void getNum () {
- System.out.println(Thread.currentThread().getName()+"----"+i);
- }
- public static void main (String[] args) {
- new Thread(() -> {
- SynchroizedStaticMethod.addNum();
- },"addNum").start();
- new Thread(() -> {
- SynchroizedStaticMethod.getNum();
- },"getNum").start();
- }
- }
一部分执行结果
- addNum----92
- addNum----93
- addNum----94
- addNum----95
- addNum----96
- addNum----97
- addNum----98
- addNum----99
- addNum----100
- getNum----100
- Process finished with exit code 0
(3) 对于同步代码块, 锁就是 Synchronized 括号里面配置的对象. 如下代码实例:
解释: 通过如下代码可以证明锁就是括号里面的对象, 当两个方法是一个对象时, 只能是获取到对象锁的方法 执行, 但是是两个锁对象时, 那么两个方法获取的就是不同的锁对象, 所以结果不一样了.
- /**
- * 代码块
- */
- public class SynchroizedCodeBlock {
- private Object object = new Object();
- public void printOne () {
- synchronized (object) {
- for (int i = 0; i <10; i++) {
- System.out.println(Thread.currentThread().getName() + "---" + 1);
- }
- }
- }
- public void printTwo () {
- synchronized (object) {
- System.out.println(Thread.currentThread().getName()+"---"+2);
- }
- }
- public static void main (String[] args) {
- SynchroizedCodeBlock codeBlock = new SynchroizedCodeBlock();
- new Thread(() -> {
- codeBlock.printOne();
- },"printOne").start();
- new Thread(() -> {
- codeBlock.printTwo();
- },"printTwo").start();
- }
- }
执行结果
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printTwo---2
- Process finished with exit code 0
改变下括号里面的对象
- public void printTwo () {
- synchronized (this) {
- System.out.println(Thread.currentThread().getName()+"---"+2);
- }
- }
执行结果 (与第一次不一样了)
- printTwo---2
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- printOne---1
- Process finished with exit code 0
3. 锁在什么地方 (Java 对象头)
Synchronized 用的锁是存在 Java 的对象头里的. 如果对象时数组类型, 则虚拟机用 3 个字宽存储对象头..Java 对象头里的 Mark Word 里默认储存对象的 HashCode. 分代年龄和锁标记位
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的 hashcode 或锁信息等 |
32/64bit | Class Metadata Address | 存储对象数据类型的指针 |
32/64bit | Array length | 数组的长度 (如果当前对象时数组) |
Mark Word 的状态变化表
4.JSE1.6 对锁的优化 (锁的升级与对比)
在 Java SE1.6 中, 锁一共有 4 中状态, 级别从低到高依次是: 无锁状态, 偏向锁状态, 轻量级锁状态和重量级锁状态, 这几个状态会随着竞争情况逐渐升级. 锁可以升级但不能降级, 意味着偏向锁升级成轻量级锁后不能降级成偏向锁. 这种锁升级却不能降级的策略, 目的是为了提高获得锁和释放锁的效率.
(1) 偏向锁
why: 在大多数情况下, 锁不仅不存在多线程竞争, 而且总是由同一个线程多次获得, 为了让线程获得锁的代价更低而引入了偏向锁.
what: 当一个线程访问同步块并获取锁时, 会在对象头和栈帧中的锁记录里储存偏向的线程 ID, 以后该线程在进入和退出同步代码块时不需要进行 cas 操作来加锁和解锁, 只需要简单地测试一下对象头的 Mark Word 里是否储存着指向当前线程的偏向锁. 如果测试成功, 表示该线程获得了锁. 如果测试失败, 则需要在测试一下 Mark Word 中偏向锁的表示是否设置成 1(表示当前是偏向锁): 如果没有设置, 则使用 cas 竞争锁; 如果设置了, 则尝试使用 cas 将对象头的偏向锁指向当前线程.
偏向锁的撤销: 偏向锁使用了一种等到竞争出现才释放锁的机制, 所以当其它线程尝试竞争偏向锁时, 持有线程才会释放锁. 偏向锁的撤销, 需要等待全局安全节点 (在这个时间点上没有正在执行的字节码).
偏向锁的升级: 如果有线程来竞争偏向锁, 那么就需要判断对象头的 Mark Word 的线程 ID 和当前线程 ID 是否一致, 如果不一致说明发送了竞争, 那么就需要检查拥有偏向锁的线程是否还存活; 如果没有存活, 那么将对象头设置为无锁状态, 当前线程和其它线程都可以去竞争偏向锁; 如果存活, 暂停拥有偏向锁的线程, 遍历栈帧信息, 判断这个线程是否还要使用这个锁对象, 如果还需要, 就撤销偏向锁, 升级为轻量锁, 如果不要继续使用, 标记为无锁状态, 重新偏向其它线程. 如果升级为轻量锁后, 应该还是拥有锁的线程先去执行.
(2) 轻量锁
why: 轻量锁是为线程竞争不是很多, 每个线程的执行时间不长而准备的, 因为轻量锁发生竞争时, 不阻塞线程, 而是采用的自旋; 如果竞争时就阻塞线程, 而锁很快就释放了, 这个线程的状态切换也是很大的消耗.
waht: 线程在执行同步代码块前, jvm 会先在当前线程的栈帧中创建一个用于存储锁记录的空间, 并将对象头中的 Mark Word 替换为为指向锁记录的指针. 如果成功, 当前线程获取锁, 如果失败, 表示其它线程竞争锁, 当前线程尝试使用自旋来获取锁. 其实就在当前线程里面存了一份拷贝的对象头的 Mark Word(官方叫 displaced Mark Word), 然后把对象头里面 Mark Word 指针指向了当前线程在一开始创建的一块锁记录空间, displaced Mark Word 是在释放锁时恢复无锁用的. 这一块其实有些绕, 就是怎么判断锁这一块具体参考这篇文档
轻量锁的解锁: 轻量级解锁时, 会使用 cas 操作将 disolaced Mark Word 替换回到对象头, 如果成功, 则表示没有发生竞争. 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁. 过程如下图所示:
(3) 锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额为的消耗,和执行非同步方法相比,仅存在纳秒级别的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块场景 |
轻量锁 | 竞争线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗 cpu | 追求响应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不消耗 cpu | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
5. 总结 (有些个人理解)
偏向锁在 Java 6 和 Java 7 是默认开启的, 但是它在应用程序几秒钟之后才激活, 如果有必要可以使用 jvm 的参数来关闭延迟:-XX:BiasedLockingStartupDelay=0. 偏向锁是在单线程的时候使用的, 我们知道单线程其实可以不用使用同步, 我的理解就是为了预防线程安全问题, 因为可能会出现多线程的情况, 所以我们要预防以免带来线程安全的问题; 从上面表格可以看出加上偏向锁性能在纳秒级别, 完全可以接受; 偏向锁设置是把 Mark Word 的是否为偏向锁的标识使用 cas 设置为 1, 然后使用设置偏向锁的里面的线程 ID, 设置成功, 获取锁, 也不用释放锁, 下次这个线程再来获取锁, 只需要判断偏向锁里面有没有自己的线程 ID 即可; 但是如果有线程在 Mark Word 的是否为偏向锁的标识为 1 时, 有线程来竞争锁, 那么 cas 设置对象头的偏向锁指向自己线程 ID 时就会失败, 因为有线程已经指向了, 那么这个时候就不满足偏向锁了, 这时就需要执行锁撤销的步骤, 首先等待全局安全点, 然后暂停拥有偏向锁的线程, 判断当前拥有锁的线程是否还是存活状态, 如果没有存活, 将对象头设置无锁状态; 如果还存活, 判断线程是否还需要执行代码块, 从栈帧中找到锁记录, 如果当前线程还需要执行同步代码块, 就证明需要锁, 这个时候升级为轻量锁, 然后唤醒暂停线程, 竞争线程开始自旋; 如果不需要执行同步代码块了, 要么恢复到无锁或者标记对象不适合偏向锁 (出现竞争了). 轻量锁, 说下锁升级, 轻量锁在释放锁时, 会使用原子的 cas 把原来线程里面的 displaced Mark Word 替换为对象头, 但是如果替换失败, 表示锁竞争, 锁就升级为重量级锁, 这个过程为什么会失败了, 是因为轻量锁的自旋是有次数的, 如果达到一定的次数还没有成功, 这个自旋的线程就会升级为重量锁, 那么此时的对象头的 Mark Word 就会被改变, 然后暂停该线程, 当前面的轻量锁在释放锁时, 会发现对象头的 Mark Word 被修改, 然后升级为重量锁, 唤醒之前暂停的线程.
选自《Java 并发编程的艺术》
和参考两位大佬的博客
锁的升级
Synchronized 整个锁的流程图 (厉害) https://www.jianshu.com/p/3aac4239a84c
来源: http://blog.51cto.com/14220760/2366218