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()的调用轨迹:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. 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()调用轨迹如下:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)
  3. 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()调用轨迹如下:

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. 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);
    }
}

本博客所有文章除特别声明外,大部分为学习心得,欢迎与博主联系讨论