Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。同步代码块包括两部分:一个作为锁的对象引用,一个作为由这个锁保护的代码块。以synchronized关键字来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
Java的内置锁是一个互斥锁,这意味着最多只有一个线程能持有这种锁。由于每次只能有一个线程执行内置锁保护的代码块,因此由这个锁保护的同步代码块会以原子的方式执行,多个线程在执行该代码块时互不干扰。
synchronized的用法
同步方法
同步非静态方法
synchronized void methodName() {
...
}
对于普通方法,锁是当前实例对象。
同步静态方法
static synchronized void methdName() {
...
}
对于静态方法,锁是当前类的Class对象。
同步代码块
获取对象锁
synchronized(this.Object) {
...
}
在Java中,每个对象都会有一个monitor对象,这个对象其实就是Java对象的锁,称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。类的对象有多个,所以每个对象有其独立的对象锁,互不干扰。
获取类锁
private final Object Clazz = new Object();
synchronized(Clazz.class) {
...
}
在Java中,针对每个类也有一个锁,称为“类锁”,类锁实际上是通过对象锁实现的,即类的Class对象锁。每个类只有一个Class对象,所以每个类只有一个类锁。
示例代码:
public class SynchroDemo {
// 同步非静态方法
public synchronized void test1() {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 同步代码块
public void test2() {
synchronized (this) {
try {
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + " is runing");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
SynchroDemo demo = new SynchroDemo();
for (int i = 0; i < 5; i++) {
new Thread(()->{
demo.test1();
}).start();
}
}
}
synchronized原理分析
在Java中,每个对象都会有一个monitor对象,监视器。
- 某一线程想要获取某个锁时,先判断该锁对应的moitor计数器是不是0,如果是0表示该锁还没有被任何线程持有,这个时候线程占有这个锁,并且mointor+1;如果不为0,表示这个锁已被其他线程占有,此线程等待。当线程释放占有权的时候,moitor-1,当monitor为0时,这个锁将被释放。
- 同一线程可以对同一个对象进行更多次加锁,可重入性。
下面我们通过javap指令来看下synchronized的底层实现
反编译上面的示例代码。
> javap -v SynchroDemo
可以看到同步代码块是使用mointorenter
和monitorexit
指令来实现的。
同步方法是通过设置一个ACC_SYNCHRONIZED
标志来实现的。
JVM对synchronized的优化
在多线程并发编程中synchronized
一直是元老级角色,很多人都会称呼它为重量级锁。从Java SE 1.6开始对synchronized
进行了各种优化,为了减少获得锁和释放锁带来的性能消耗而引入了偏向锁和轻量级锁的概念。
Java对象头
synchronized
用的锁是存在Java对象头里的。
Java对象头的长度
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashcode或锁信息等 |
32/64bit | Class Metadata Address | 存储到对象类型数据的指针 |
32/64bit | Array length | 数据的长度(如果当前对象是数组) |
Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Work可能变化为存储以下4种数据:
锁的升级过程
在Java SE 1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级,锁可以升级但是不能降级。
无锁状态
没有加锁
偏向锁
当一个线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程访问同步块获取锁时不需要进行CAS操作来加锁和解锁,只需简单测试下对象头里的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已获得了锁。如果测试失败,则需要在测试一下Mark Word中偏向锁的标识是否设置成了1:如果没有则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指针指向当前线程;如果竞争失败,则偏向锁会进入撤销过程,对象头中的Mark Work恢复为无锁标记,此时会升级为轻量级锁。
轻量级锁
线程在进入同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,当前线程尝试使用自旋来获取锁。
轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生。如果失败,锁就会升级为重量级锁。
重量级锁
当锁处于这个状态,其他线程试图获取锁时都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新一轮的锁争夺。(此时不存在轻量级锁中的自旋,线程是直接挂起)
锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销消耗 | 适用于只有一个线访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求相应时间,同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行时间较长 |
参考资料
Java并发编程的艺术 方腾飞 魏鹏 程晓明 著
Java并发编程实战 童云兰 译