Java中的锁
Java中的锁
Volatile
Volatile实现原理
在有volatile变量修饰的共享变量进行写操作时汇编会多出下行代码:
0x01a3de24: lock add1 $0×0,(%esp)
lock前缀的指令在多核处理器中会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存。
- 写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
Volatile内存语义
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
synchronized
synchronized实现原理
重量级锁
- JVM基于进入和退出Monitor对象来实现同步和代码块同步。代码块同步是使用monitorenter和monitorexit来实现的,而方法同步是使用另外一种方法来实现的,细节在JVM规范中没有说明。但方法同步依旧可以用这两个指令实现。
- 当JVM执行引擎执行某一个方法时,其会从方法区中获取该方法的access_flags,检查其是否有ACC_SYNCRHONIZED标识符,若是有该标识符,则说明当前方法是同步方法,需要先获取当前对象的monitor,再来执行方法。
- monitorenter指令插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。JVM保证每个monitorenter都有对应的monitorexit与之配对。任何一个对象都有一个monitor与之关联,当一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。若获取失败,那么线程将会进入同步队列(SynchronizedQueue),在正在占有monitor的线程离开监视器后,将会通知在同步队列中的线程获取锁。
- 优点:线程竞争不使用自旋,不会消耗CPU。
- 缺点:线程阻塞,通过唤醒的方式来提醒其他线程,响应速度缓慢。
偏向锁
- 大多情况下,锁不仅不存在多线程竞争,而且总是由同一个线程获得。那么为了获取锁和是释放锁而频繁地进行CAS操作将会显得浪费资源。
- 偏向锁在对象头中可以储存线程的ID,以后在进入后退出同步块的时候就只需要比对对象头中的Mark Word中是否储存着当前线程的偏向锁,如果测试成功,就表示线程已经获得了锁。则检测是否为偏向锁,若不是则使用CAS竞争锁;如果是偏向锁,则尝试使用CAS将对象头的偏向锁指向当前线程。
- 偏向锁的撤销
- 偏向锁在遇到竞争时才会撤销
- 撤销时,它会先暂停持有锁的线程,然后检测线程是否处于活动状态。
- 若是不处于活动状态,则将对象头设为无锁状态。前来竞争的线程获取锁,使用CAS将偏向锁ID替换为自己的。(此时依旧是偏向锁)
- 如果线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。(此时偏向锁考虑升级为轻量级锁)
- 需要注意的是,偏向锁适用于同步块在一个时间段内只被一个线程使用的情况,用于减少获得锁和释放锁的CAS操作,属于乐观锁。若是存在竞争,由于偏向锁的撤销会发生STW操作,将会使效率大大降低。
- 优点:加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距。
- 缺点:如果存在锁竞争,将会带来额外的锁撤销的消耗。
轻量级锁
- 加锁:线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。线程尝试使用CAS将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程竞争锁,当前线程尝试使用自旋来获取锁。
- 解锁:使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。一旦膨胀为重量级锁,将不再恢复到轻量级锁状态。锁膨胀后,持有锁的线程在释放锁并唤醒竞争的线程,进行新一轮的夺锁。
- 优点:竞争的线程不会阻塞,提高程序的响应速度。
- 缺点:始终得不到锁竞争的线程,使用自旋将会消耗CPU
ReentrantLock的加锁和解锁原理
ReentrantLock的实现依赖于Java同步器框架AQS。AQS使用一个整型的volatile变量(命名为state)来维护同步状态。ReentrantLock分为公平锁和非公平锁。
公平锁
使用公平锁时,加锁方法lock()
的调用轨迹:
- ReentrantLock:lock()
- FairSync:lock()
- AbstractQueuedSynchronizer:acquire(int arg)
- ReentrantLock:tryAcquire(int acquires)
从第4步开始加锁,源码如下:
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//通过AQS获取同步状态
int c = getState();
//同步状态为0,说明临界区处于无锁状态,
if (c == 0) {
//判断该线程是否在队首,然后修改同步状态,即加锁
if (isFirst(current)&&compareAndSetState(0, acquires)) {
//将当前线程设置为锁的owner
setExclusiveOwnerThread(current);
return true;
}
}
//如果临界区处于锁定状态,且上次获取锁的线程为当前线程
else if (current == getExclusiveOwnerThread()) {
//则递增同步状态
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
其解锁方法unlock()
调用轨迹如下:
- ReentrantLock:unlock()
- AbstractQueuedSynchronizer:release(int arg)
- Sync:tryRelease(int releases)
第3步才开始真正释放锁,源码如下:
protected final boolean tryRelease(int releases){
int c = getState() - releases;
if(Tread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException()
boolean free = false;
if(c == 0){//如果state减去releases为0,则释放锁
free = true;
setExcluiveOwnerThread(null);//将获取锁的线程设为null,即无锁
}
setState(c);//设置state,若c为0,则释放锁
return free;
}
非公平锁
使用非公平锁时,加锁方法lock()调用轨迹如下:
- ReentrantLock:lock()
- NonfairSync:lock()
- AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)
第3步进行加锁。该方法使用原子操作的方式更新state变量。源码如下:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))//对state进行CAS
//若成功则加锁
setExclusiveOwnerThread(Thread.currentThread();
else
acquire(1);
}
}
本博客所有文章除特别声明外,大部分为学习心得,欢迎与博主联系讨论