#yyds干货盘点#面试官让我聊聊synchronized

Posted Liziba

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了#yyds干货盘点#面试官让我聊聊synchronized相关的知识,希望对你有一定的参考价值。

1、简介

synchronized是Java并发领域元老级人物,synchronized很多程序员都会用,它有三种表现形式。

  • 普通同步方法 -> synchronized锁住的是当前对象
private synchronized void demo() {
// todo
}
  • 静态同步方式 -> synchronized锁住的是当前类的Class对象
private static synchronized void demo() {
// todo
}
  • 同步代码块 -> synchronized锁住的是代码块括号中声明的对象
// 锁住的是SynchronizedDemo类的Class对象
private void demo1() {
synchronized (SynchronizedDemo.class) {
// todo
}
}
// 锁住的是当前对象,也可以是任意对象,Java中每一个对象都可以作为锁
private void demo1() {
synchronized (this) {
// todo
}
}

synchronized在很多人眼里都是性能低的并发实现方式,早期Java中synchronized确实是一把重量级锁,在JDK1.6之后对synchronized做了全面的优化,优化主要思路是:同步代码块大多数场景下并不存在多线程竞争的情况,通俗点说就是大部分情况下这把锁其实不需要。因此JDK1.6引入了“偏向锁”和“轻量级锁”,从此以后synchronized中锁一共有4个状态,分别是无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁的四个状态是一个升级打怪的过程,只能不断升级而不能降级,当锁已经成为重量级锁了,就无法回到轻量级锁。



2、锁升级

2.1 无锁状态

在32位虚拟机中,无锁状态的对象头中的Mark Word组成如下所示(对象头不了解可以查看本专栏中的Monitor文章)

#yyds干货盘点#面试官让我聊聊synchronized_sed

在64位虚拟机中,无锁状态的对象头中的Mark Word组成如下所示(32位虚拟机和64位虚拟机对应锁的升级过程在实现逻辑上没有什么区别,目前大多数操作系统都是64位操作系统,因此64位虚拟机使用也更加广泛一些)

#yyds干货盘点#面试官让我聊聊synchronized_初始状态_02

一个对象初始状态都是无锁状态,我们主要关注最后三位:

  • biased_lock占一位,表示是否是偏向锁,初始值为0,表示不是偏向锁
  • lock_state占两位,表示锁标志位或锁状态,初始值为10,表示无锁状态

默认情况下开启偏向锁,因此如果不关闭偏向锁,上述biased_lock值应该为1。


2.2 偏向锁

为什么会设计偏向锁这个东西呢?

这是因为大多数情况下,同步代码压根就没有竞争的情况发生,也就是一把锁一直是同一个线程在加锁、执行同步代码、解锁,这种情况是不是可以优化呢?当然可以啦!那怎么优化呢?这就是偏向锁干的事情。

比如如下代码,在未使用偏向锁的情况下需要两次加锁解锁操作,而使用偏向锁则免去了这些操作。

final Object lock = new Object();

private void lockFirst() {
// 使用CAS将线程ID设置到对象头中的Mark Word中
synchronized (lock) {
// todo
}
lockSecond();
}


private void lockSecond() {
// 比较锁对象头的Mark Word中是不是偏向当前线程即可
synchronized (lock) {
// todo
}
}


什么是偏向锁呢?

从字面上就能理解,偏向锁就是偏向某个线程的锁,将这个锁对象想办法标记为当前线程就可以了,线程怎么区分呢?就用线程ID做标记嘛,把线程ID搞到锁对象里面就可以了嘛!


具体怎么实现的呢?

当某个线程访问同步代码需要获取锁时,不再直接去关联一个monitor对象,而是使用CAS将线程ID设置到对象头中的Mark Word中,并且线程栈帧中的锁记录中也会存储锁偏向的线程ID,这样只要锁不发生竞争,同一个线程多次尝试获取同一把锁的时候,只需要比较锁对象头的Mark Word中是不是偏向当前线程即可。

在32位虚拟机中,处于偏向锁的对象头的Mark Word组成如下所示:

前23位被设置成偏向线程的ID,biased_lock被设置成1,表示当前锁对象处于偏向锁状态,指的注意的是偏向锁的锁标志位和无锁标志位是一样都是10

#yyds干货盘点#面试官让我聊聊synchronized_无锁_03

在64位虚拟机中,处于偏向锁的对象头的Mark Word组成如下所示:

前54位被设置成偏向锁ID,biased_lock被设置成1

#yyds干货盘点#面试官让我聊聊synchronized_加锁_04


眼见为实,如何查看?

jdk提供了对应的类来查看打印对象的内存信息,引入jol依赖

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>LATEST</version>
</dependency>

测试代码:

public static void main(String[] args) throws InterruptedException {

Object lock = new Object();
log.info(ClassLayout.parseInstance(lock).toPrintable());

}

输出结果:

#yyds干货盘点#面试官让我聊聊synchronized_内存地址_05

可以看出我的虚拟机是64位虚拟机,因为对象头的Mark Word占用8个字节,但是输出的Mark Word值怎么是001,不是101呢?你不是说默认开启偏向锁么?

这是因为偏向锁的开启,虚拟机采用了延迟启动。不过这个延迟启动偏向锁,我们可以通过VM参数-XX:BiasedLockingStartupDelay=0来关闭。

#yyds干货盘点#面试官让我聊聊synchronized_初始状态_06

此时MarkWord的值为0x0000000000000005,转换为二进制就是101了,这就证明了上面我们说的那些知识点啦。除此之外我们可以通过-XX:-UseBiasedLocking来关闭偏向锁(-XX:+UseBiasedLocking为启动偏向锁)。

关闭延迟偏向锁,以及关闭偏向锁 -XX:-UseBiasedLocking -XX:BiasedLockingStartupDelay=0

#yyds干货盘点#面试官让我聊聊synchronized_加锁_07

从输出结果来看,偏向锁被禁用了

#yyds干货盘点#面试官让我聊聊synchronized_初始状态_08


偏向锁的展示

偏向锁的威力在于当锁偏向于某个对象时,此时只需要比较线程id即可,看一段测试代码:

static final Object lock = new Object();

public static void main(String[] args) throws InterruptedException {

log.info("初始状态...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
Thread t1 = new Thread(() -> {
synchronized (lock) {
log.info("线程t1第一次持有锁时...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
}

synchronized (lock) {
log.info("线程t1第二次持有锁时...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
}

}, "Thread-1");

t1.start();
t1.join();
log.info("线程t1加锁之后...");
log.info(ClassLayout.parseInstance(lock).toPrintable());

}

共64位,高位请脑海里补0

初始Mark Word为0x0000000000000005 -> 二进制 100000000000000000000000000101

偏向线程t1之后为0x000000002022b805 -> 二进制 100000001000101011100000000101

可以看到此时Mark Word中标记了线程ID (注意这个线程id是操作系统分配的线程id,不是虚拟机中java给定的线程id,不信你试试看),重复获取同一把锁t1线程只需要比较线程id即可。

#yyds干货盘点#面试官让我聊聊synchronized_初始状态_09


hashcode()和偏向锁的关系

先看一段测试代码

@Slf4j
public class HashCodeAndBiasedLock {

static final Object lock = new Object();

public static void main(String[] args) {
log.info("调用HashCode之前");
log.info(ClassLayout.parseInstance(lock).toPrintable());
lock.hashCode();
log.info("调用HashCode之后");
log.info(ClassLayout.parseInstance(lock).toPrintable());
}

}

输出结果:

#yyds干货盘点#面试官让我聊聊synchronized_内存地址_10

可以看到Mark Word的变化(共64位,高位请脑海里补0):

  • 初始默认开启偏向锁0x0000000000000005 -> 10000000000000000000000000000000101
  • 调用hashcode()方法0x000000063e31ee01 -> 11000111110001100011110111000000001
  • 线程id0x063e31ee -> 110001111100011000111101110


可以看到调用hashCode()方法之后,MarkWord的最低3位由101转换为001了,偏向锁被取消了,此时Mark Word的组成就由下图所示。

#yyds干货盘点#面试官让我聊聊synchronized_sed_11

因此可以得出结论,当我们调用某个锁对象的hashCode()方法时,默认的偏向锁机制将会被取消。



偏向锁升级为轻量级锁

偏向锁在什么时候会升级为轻量级锁呢?

可以看如下代码,t1对lock对象加锁之后,t2对lock对象加锁。

static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {

log.info("初始状态...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
Thread t1 = new Thread(() -> {

log.info(String.valueOf(Thread.currentThread().getId()));
synchronized (lock) {
log.info("线程t1第一次持有锁时...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
}

}, "Thread-1");


t1.start();
t1.join();

log.info("线程t1加锁之后...");
log.info(ClassLayout.parseInstance(lock).toPrintable());

Thread t2 = new Thread(() -> {
synchronized (lock) {
log.info("线程t2持有锁时...");
log.info(ClassLayout.parseInstance(lock).toPrintable());
}
}, "Thread-2");

t2.start();
t2.join();
#yyds干货盘点# Java并发面试题第二弹

#yyds干货盘点# 京东二面,Redis为什么这么快?

因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!

#yyds干货盘点#聊聊javascript——callapplaybind

#yyds干货盘点# 面试篇:虚拟机栈5连问,一听心里就乐了

#yyds干货盘点#今天聊聊大文件上传