【面试专栏】JAVA锁机制
阅读原文时间:2023年07月08日阅读:2

1. 悲观锁 / 乐观锁

  在Java和数据库中都存在悲观锁和乐观锁的应用。Mysql锁机制中的悲观锁和乐观锁请查看:

  Mysql锁机制--悲观锁和乐观锁

  悲观锁:在获得数据时先加锁,只到数据操作(更新)完成,确保不会被其他线程所影响。例如:Java中synchronized关键字和Lock的实现类都是悲观锁。

  乐观锁:在获得数据时不会加锁,而是在操作数据时判断数据是否被修改过,因此可能会出现线程抢占的情况。当数据未被更新时,直接更新数据;当数据被更新后,抛出异常或通过程序自旋重试解决。

  从上述中可以得出:

  悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确性;

  乐观锁适合读操作多的场景,不加锁可以使其读操作的性能大幅提升。

  悲观锁和乐观锁使用示例:

// 悲观锁 - synchronized
public synchronized void sync() {
    // ...
}

// 悲观锁 - Lock
private Lock lock = new ReentrantLock();// 多线程公用一把锁

public void lock() {
    try {
        // 加锁
        lock.lock();
        // ...
    } finally {
        // 释放锁
        lock.unlock();
    }
}

// 乐观锁 - AtomicInteger
private AtomicInteger atomicInteger = new AtomicInteger();

public void plus() {
    atomicInteger.getAndIncrement();
}

  从上述示例中可以看出,悲观锁都是显式加锁后再操作同步资源,乐观锁直接操作同步资源。乐观锁的主要实现是CAS原理,请查看:

  【面试专栏】JAVA CAS(Conmpare And Swap)原理

2. 自旋锁 / 适应性自旋锁

  自旋锁:Java线程的阻塞、挂起和唤醒需要操作系统CPU状态切换才可完成,如果同步代码非常简单且耗时很短,则可能CPU状态切换的耗时比同步代码的耗时更长,无疑浪费了时间和性能。因此,为了能让线程暂停等待而不阻塞,则需要线程进行自旋,即无限循环当满足某种条件时跳出循环再继续执行。这种避免线程切换的开销方式,就是自旋锁。

  自旋锁的主要实现也是CAS原理。AtomicInteger的自增调用的Unsafe中getAndAddInt方法,查看OpenJDK 8中Unsafe中getAndAddInt方法源码:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatitle(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    return var5;
}

  从上述代码中可以看出,do-while就是自旋操作。当修改失败后,通过循环自旋直到修改成功。

  自旋锁虽然避免了线程切换的问题,但是会占用CPU时间。如果自旋时间很短,则是非常好的解决方法;当自旋时间过长时,无疑会浪费CPU资源。因此,自旋的次数要有一定的限制,当超过限制后则直接挂起线程等待。JDK1.6中默认开启自旋次数(10),并且引入了自适应的自旋锁(适应性自旋锁)。

  适应性自旋锁:适应其实意味着自旋的次数不再受限,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,线程通过自旋成功获得锁,并且持有锁的线程正在运行中,那么虚拟机认为本次自旋很有可能再次成功,因此允许本线程自旋等待。如果对于某个锁,自旋成功几率很小,那在以后尝试获取该对象锁时可能不再自旋,直接阻塞线程,避免浪费CPU资源。

3. 无锁 / 偏向锁 / 轻量级锁 / 重量级锁

  JDK1.6之前synchronized是重量级锁,效率很低,JDK1.6后,对其进行优化,引入了无锁、偏向锁、轻量级锁、重量级锁。

  无锁:CAS原理。不对资源进行加锁,所有线程都能访问并修改同一资源,但同时只有一个线程能修改成功。修改操作循环尝试获得锁,如果可以获得,则直接操作,否则继续循环。

  偏向锁:指一段同步代码一直被一个线程访问,那么该线程会自动获取锁,降低获取锁的代价。当其他线程尝试竞争偏向锁时,持有偏向锁的线程才释放锁,线程不会主动释放偏向锁。

  轻量级锁:指当锁为偏向锁时有其他线程尝试获得锁,偏向锁会升级为轻量级锁,其他线程通过自旋方式尝试获取锁,但不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋尝试等待。但当自旋超过一定的次数,轻量级锁又升级为重量级锁。

  重量级锁:等待锁的其他线程都会进入阻塞状态。

  以为其实是锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁。

4. 公平锁 / 非公平锁

  公平锁:线程按照顺序依次获取锁。优点:等待线程都会获得锁;缺点:其他等待线程都会阻塞,线程阻塞唤醒引起的CPU状态切换从而消耗资源。

  非公平锁:先尝试获得锁,获取失败则采用公平锁方式。优点:减少线程阻塞唤醒引起的CPU状态切换从而消耗资源,提高吞吐量;缺点:等待线程可能很长时间或永远获得不到锁。

  分析ReentrantLock部分源码:

public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        //......
    }

    /**
     * Sync object for non-fair locks
     */
    static final class NonfairSync extends Sync {
        //......
    }

    /**
     * Sync object for fair locks
     */
    static final class FairSync extends Sync {
       //......
    }

    /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

    //......

}

  从源码可以看出,ReentrantLock中有一个内部类Sync,加锁和释放锁都是由Sync实现的:公平锁FairSync、非公平锁NonfairSync。通过构造函数可以看出,ReentrantLock默认使用公平锁,当带参构造传递false时,获得非公平锁。   再查看公平锁和非公平锁源码:

// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 非公平锁
/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            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;
}

  可以看出,公平锁和非公平锁的区别在于,是否添加了hasQueuedPredecessors(是否存在前置任务),再查看hasQueuedPredecessors源码:

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

  可以看出,此方法判断当前线程是否是同步队列中的第一个。因此得出,公平锁先判断是否为同步队列中第一个,则获得锁,否则排队等待;非公平锁先抢占,抢占成功,则获得锁,抢占失败,则排队。

5. 可重入锁 / 非可重入锁

  可重入锁:又名递归锁。指同一线程在外层方法中获取锁,内层函数自动获取锁(同一对象或Class)。优点:避免死锁。

  Java中ReentrantLock和synchronized都是可重入锁,为例分析:

/**
 * 同步方法一
 */
public synchronized void method1() {
    // ......
    method2();
    // ......
}

/**
 * 同步方法二
 */
public synchronized void method2() {
    // ......
}

  在上述方法中,method1和都被synchronized修饰,当调用method1时,获取当前锁,method1调用method2,根据可重入锁规则,自动获取锁。

  假如使用非可重入锁,当进入method1获取锁,在进入method2之前,先要释放当前对象锁,但是当前对象锁被当前线程所持有,无法释放,则出现死锁。

  可重入锁ReentrantLock和非可重入锁NonReentrantLock都继承AQS(AbstractQueuedSynchronizer),AQS中存在一个属性state来表明同步状态(可重入锁时表示重入次数)。

public class ReentrantLock implements Lock, java.io.Serializable {

    /** Synchronizer providing all implementation mechanics */
    private final Sync sync;

    /**
     * Base of synchronization control for this lock. Subclassed
     * into fair and nonfair versions below. Uses AQS state to
     * represent the number of holds on the lock.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
    ......
    }
    ......
}


public final class NonReentrantLock extends AbstractQueuedSynchronizer implements Lock {
......
}


public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     * The synchronization state.
     */
    private volatile int state;

    /**
     * Returns the current value of synchronization state.
     * This operation has memory semantics of a {@code volatile} read.
     * @return current state value
     */
    protected final int getState() {
        return state;
    }

    /**
     * Sets the value of synchronization state.
     * This operation has memory semantics of a {@code volatile} write.
     * @param newState the new state value
     */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     * Atomically sets synchronization state to the given updated
     * value if the current state value equals the expected value.
     * This operation has memory semantics of a {@code volatile} read
     * and write.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that the actual
     *         value was not equal to the expected value.
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
}

  通过源码分析下为什么非可重入锁在重复调用同步资源时会出现死锁。首先查看可重入锁获取锁源码:

/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            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;
}

  可以看出,可重入锁尝试获取锁时,先获取同步次数state,当state=0时,表示没有其他线程执行当前同步方法,则修改状态state=1,表示正在执行。当state!=0时,判断当前线程是否已经获得当前锁,若是则state+1表示再次获取锁。

  查看非重入锁获取锁源码:

protected boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, 1)) {
        owner = Thread.currentThread();
        return true;
    }
    return false;
}

  可以看出,非重入锁直接获取当前锁,当state !=0时,获取失败,进入死锁。

  再查看释放锁的过程。首先查看可重入锁释放锁源码:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

  可以看出,可重入锁在当前线程独占所有锁时,判断state-1=0,若true则表明当前线程所重复获取锁已经全部执行完成,释放锁。

  查看非重入锁释放锁源码:

protected boolean tryRelease(int releases) {
    if (Thread.currentThread() != owner) {
        throw new IllegalMonitorStateException();
    }
    owner = null;
    setState(0);
    return true;
}

  可以看出,非重入锁在当前线程独占所有锁时,直接将state置为0,释放锁。

6. 独享锁 / 共享锁

  共享锁:又名排它锁。指当前锁只能被一个线程所持有。例如线程T1对同步资源A添加独享锁后,其他线程不能对同步资源A添加任何锁。获得独享锁的线程既能读数据也能写数据。

  共享锁:指当前线程可以被多个线程所持有。例如线程T1对同步资源A添加共享锁后,其他线程可对该资源添加共享锁,但不能添加独享锁。获得共享锁的线程只能读数据,不能写数据。

  通过ReentrantReadWriteLock源码来分析独享锁和共享锁:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    private static final long serialVersionUID = -6992448646407690164L;
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * default (nonfair) ordering properties.
     */
    public ReentrantReadWriteLock() {
        this(false);
    }

    /**
     * Creates a new {@code ReentrantReadWriteLock} with
     * the given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantReadWriteLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
        readerLock = new ReadLock(this);
        writerLock = new WriteLock(this);
    }

    public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
    public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

    /**
     * Synchronization implementation for ReentrantReadWriteLock.
     * Subclassed into fair and nonfair versions.
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        ......
    }
    ......
}


/**
 * The lock returned by method {@link ReentrantReadWriteLock#readLock}.
 */
public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    private final Sync sync;

    /**
     * Constructor for use by subclasses
     *
     * @param lock the outer lock object
     * @throws NullPointerException if the lock is null
     */
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    ......
}


/**
 * The lock returned by method {@link ReentrantReadWriteLock#writeLock}.
 */
public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;

    /**
     * Constructor for use by subclasses
     *
     * @param lock the outer lock object
     * @throws NullPointerException if the lock is null
     */
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
    ......
}

  可以看出ReentrantReadWriteLock中有两种锁:读锁(ReadLock)、写锁(WriteLock)。读锁和写锁都是由Sync实现的,Sync是AQSAbstractQueuedSynchronizer)的子类。AQS中存在属性state来表明同步状态(0/1)或者持有锁的数量,但ReentrantReadWriteLock中存在两种锁,因此需要将state分割,来表明读锁和写锁的数量。AQS中state为int类型,占4个字节,即32位,因此将state分割为高16位(读锁数量)和低16位(写锁数量)。

  首先查看写锁添加锁的源码:

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

  可以看出,在尝试获取锁时先获取锁的总数c,通过总数c再获取了写锁的数量w。

  首先判断c不等于0,即已经有线程已经获得了锁;当写锁数w等于0(即存在读锁)或者当前线程不是该锁的持有者,则获取失败;当写锁数量超过MAX_COUNT(2的16次方 - 1),则抛出异常;否则获取锁成功。

  当锁的总数c等于0,即没有线程获取锁;当当前线程需要阻塞或者通过CAS增加写锁数量失败时,获取所失败。

  否则,设置当前锁的持有者为当前线程,返回true。

  由上述程序可以看出,当存在读锁时,写锁获取失败。因此需要等待所有持有的读锁全部释放,写锁才能获取成功,当写锁持有时,所有的读写锁均被阻塞。写锁释放与重入锁释放过程基本类似,每次释放均减少写锁数量,当写锁数量为0时表示写锁全部释放,此时等待的读写线程才能够继续访问读写锁,同时前次写线程的修改操作对后续读写线程均可见。

  查看读锁添加锁的源码:

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

  可以看出,在读锁尝试获取锁时先判断是否存在另一个线程持有写锁,是则获取失败;如果为获取写锁,则根据CAS原理增加读锁数量,成功则获取锁成功。

  通过分析读写锁获取锁源码可以得出,读读的过程共享,而读写、写读、写写的过程互斥。

  再查看下公平锁和非公平锁获取锁源码:

// 公平锁
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

// 非公平锁
/**
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            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;
}

  可以看出,公平锁和非公平锁获取锁时都会判断是否当前线程为当前锁的持有者,否则不能获取锁,即表明公平锁和非公平锁获取的都是独享锁。