美文网首页
深入理解java中的锁

深入理解java中的锁

作者: 逅弈 | 来源:发表于2018-02-06 20:31 被阅读25次

逅弈 欢迎转载,注明原创出处即可,谢谢!

我们知道在并发环境下为了保证共享变量的线程安全,除了可以使用某些原子类的操作,还可以通过为被保护的变量加锁的方式实现该变量的线程安全。

而在java中我们有两种方式来使用一个锁,请注意,这里所说的锁都是对象锁。一种是JVM帮我们实现的,通过synchronized关键字来进行加锁,另外一种是J.U.C包中的Lock接口,该接口中的两个常用的用来加锁和解锁的方法为:lock()、unlock(),除此以外还有其他的用来处理中断的锁等等。那么,对于我们使用者来说,该怎么选择使用锁就是一个需要解决的问题,而要解决这个问题,首先我们需要对这两种锁的实现原理进行深入的了解。本文笔者将会深入了解java中的这两种锁的实现原理,以期望得出一种正确使用锁的方法。

synchronized

synchronized是java中的一个关键字,通过该关键字,我们就可以很方便的对类方法、实例方法、代码块进行加锁,并且锁的释放由JVM来保证,不需要使用者执行额外的操作。

synchronized锁是基于每个对象都有一个对象监视器monitor,锁的获取和释放分别是monitorenter和monitorexit指令。
如果是在静态方法上加synchronized,则锁的对象是该类;如果在非静态方法上加synchronized,则锁的对象是该类的实例。同理,如果用synchronized(TestLock.class),则锁的对象时该类;如果用synchronized(this),则锁的对象是该类的实例,如果用synchronized(obj),则锁的对象是obj。
synchronized加锁解锁的过程可以描述为下面几个步骤:

  • 1.线程执行monitorenter指令时尝试获取monitor的所有权
  • 2.如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  • 3.如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1
  • 4.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

但是synchronized并不是对所有的请求都直接通过使用monitor来进行锁的分配,因为monitor是比较重的锁,获取不到锁的线程将进入阻塞状态,这就涉及到线程状态的切换,比较耗资源。synchronized除了实现了重量级锁之外,还实现了偏向锁,轻量级锁,其中偏向锁在1.6是默认开启的。

偏向锁、轻量级锁和重量级锁,分别是解决以下三个场景下的锁分配:

偏向锁:只有一个线程进入临界区
轻量级锁:多个线程交替进入临界区
重量级锁:多个线程同时进入临界区

synchronized的锁是保存在对象头中的,一个叫做Mark Word的数据段中,其中保存了该对象的这些信息:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。并且Mark Word被设计为一个非固定的数据结构,用来在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。

偏向锁主要是为了避免不必要的CAS操作,该锁是通过在对象头中存储一个偏向线程的ID,下一次申请锁时,先判断下当前线程ID和对象头中保存的偏向线程ID是否一致,如果一致那么表面该线程就是对象偏向的线程,可以直接获得偏向锁,但是如果偏向锁未能成功获取,则会升级到轻量级锁。

轻量级索是通过CAS操作实现的,轻量级锁假设当前环境中没有太多的并发,在没有太多线程竞争的情况下,为了避免重量级锁使用操作操作系统互斥量进行线程的挂起和恢复而造成的性能消耗。如果并发增大时,轻量级锁会升级为重量级锁,因为在高并发环境下使用轻量级索比重量级锁更耗费资源。

Lock

Lock是java并发包J.U.C中的一个用来实现锁的接口,该接口的主要实现类有ReadLock,WriteLock,ReentrantLock,实际使用过程中比较常用的就是ReentrantLock。

ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的,在AQS内部保存着一个状态变量state,通过CAS修改该变量的值,修改成功的线程表示获取到该锁,没有修改成功,或者发现状态state已经是加锁状态了,则通过一个Waiter对象封装线程,添加到等待队列中,并挂起等待被唤醒

lock的实现都委托给一个Sync类,该类继承自java.util.concurrent.AbstractQueuedSynchronizer类,而Sync类有两个子类:FairSync和NonfairSync,为了支持公平和非公平锁
AbstractQueuedSynchronizer会把所有的请求线程构成一个CLH队列,当一个线程执行完毕(lock.unlock())时会激活自己的后继节点,但正在执行的线程并不在队列中,而那些等待执行的线程全部处于阻塞状态,线程的显式阻塞是通过调用LockSupport.park()完成,而LockSupport.park()则调用sun.misc.Unsafe.park()本地方法,再进一步,HotSpot在Linux中则通过调用pthread_mutex_lock函数把线程交给系统内核进行阻塞

Lock的一些显著特点

  • 1.可重入
  1. 同一个线程多次试图获取它所占有的锁。当释放锁的时候,直到重入次数清零,锁才释放完毕
  2. lock()请求实际调用了AbstractQueuedSynchronizer.acquire(int arg)方法
  • 2.可中段
  1. 使用lock.lockInterruptibly()来响应中断,当有中断出现时,即放弃锁的请求
  • 3.公平、非公平
  1. 公平锁讲究先来先到,线程在获取锁时,如果这个锁的等待队列中已经有线程在等待,那么当前线程就会进入等待队列中
  2. 非公平锁不管是否有等待队列,如果可以获取锁,则立刻占有锁对象

lock的代码实现

ReentrantLock对象加锁时的部分调用栈:

非公平锁:

ReentrantLock/lock()

-> NonfairSync/lock()

-> AbstractQueuedSynchronizer/acquire()

-> NonfairSync/tryAcquire()

-> Sync/nonfairTryAcquire()

公平锁:

ReentrantLock/lock()

-> FairSync/lock()

-> AbstractQueuedSynchronizer/acquire()

-> FairSync/tryAcquire()

代码分析:

/**
 * Provides a framework for implementing blocking locks and related
 * synchronizers (semaphores, events, etc) that rely on
 * first-in-first-out (FIFO) wait queues.  This class is designed to
 * be a useful basis for most kinds of synchronizers that rely on a
 * single atomic {@code int} value to represent state. Subclasses
 * must define the protected methods that change this state, and which
 * define what that state means in terms of this object being acquired
 * or released.  Given these, the other methods in this class carry
 * out all queuing and blocking mechanics. Subclasses can maintain
 * other state fields, but only the atomically updated {@code int}
 * value manipulated using methods {@link #getState}, {@link
 * #setState} and {@link #compareAndSetState} is tracked with respect
 * to synchronization.
 /
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     * 以独占模式获取,忽略中断。需要实现为至少被tryAcquire方法调用一次,返回成功。
     * 如果返回不成功,则当前线程进入队列排队,可能重复的阻塞,并一直调用tryAcquire方法直到返回成功。
     * 该方法可以被用来实现lock方法
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)){
            selfInterrupt();
        }
    }
    
    /**
     * Acquires in exclusive uninterruptible mode for thread already in
     * queue. Used by condition wait methods as well as acquire.
     * 已经在等待队列中的线程以独占非中断模式获取锁
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // 如果当前节点是队列中的头节点
                // 并且通过tryAcquire成功获取了锁
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

}

/**
 * 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 {

    /**
     * Performs {@link Lock#lock}. The main reason for subclassing
     * is to allow fast path for nonfair version.
     */
    abstract void lock();
    
    /**
     * Performs non-fair tryLock.  tryAcquire is implemented in
     * subclasses, but both need nonfair try for trylock method.
     * 非公平方式tryAcquire获取锁
     */
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 获取当前的AQS的state
        int c = getState();
        // 如果state==0,说明当前还未有线程独占该锁
        if (c == 0) {
            // 直接通过CAS尝试修改state的值,
            // 如果能修改成功,则说明当前线程成功获得了锁
            // 将独占锁的线程设置为当前线程
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果state!=0,标识当前已经有线程独占该锁
        // 如果独占该锁的线程就是当前线程,则将state加上acquires之后,设置为新的state,此时说明当前线程重入了该锁
        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;
    }

}

/**
 * Sync object for non-fair locks
 * 非公平锁的实现
 */
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     * 一进来就立即通过CAS操作尝试获取锁
     * 如果获取不到再进行常规的acquire操作尝试获取锁
     */
    final void lock() {
        // 通过CAS操作尝试将当前state更改为1
        // 如果能更改成功即说明自己获得了锁,则将锁的占有者设置为当前线程
        if (compareAndSetState(0, 1)){
            setExclusiveOwnerThread(Thread.currentThread());
        }
        // 如果通过CAS未能获得锁,进行进行常规的acquire尝试来获得锁
        // acquire方法会调用tryAcquire方法
        else{
            acquire(1);
        }
    }

    /**
     * 非公平锁的tryAcquire调用的是nonfairTryAcquire方法
     */
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}


/**
 * Sync object for fair locks
 * 公平锁的实现
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     * 公平锁的尝试获取锁的方法
     * 除了当前没有其他等待获取锁的线程或者当前线程是第一个等待的,否则一直进行递归调用
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 获取当前的AQS的state
        int c = getState();
        // 如果state==0,说明当前还未有线程独占该锁
        if (c == 0) {
            // 先等待队列中的其他线程,当没有其他等待的线程了
            // 则通过CAS尝试修改state的值,
            // 如果能修改成功,则说明当前线程成功获得了锁
            // 将独占锁的线程设置为当前线程
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果state!=0,标识当前已经有线程独占该锁
        // 如果独占该锁的线程就是当前线程,则将state加上acquires之后,设置为新的state,此时说明当前线程重入了该锁
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

synchronized和lock的区别

compare synchronized lock
哪层面 虚拟机层面 代码层面
锁类型 可重入,不可中断,非公平 可重入,可中断,可公平
锁获取 A线程获得锁,B线程等待 可以尝试获得锁,不需要一直等待
锁释放 由JVM释放锁 在finally中手动释放。如不释放,会造成死锁
锁状态 无法判断 可以判断

lock比synchronized有哪些优点

  1. 支持公平锁,某些场景下需要获得锁的时间与申请锁的时间相一致,但是synchronized做不到
  2. 支持中断处理,就是说那些持有锁的线程一直不释放,正在等待的线程可以放弃等待。如果不支持中断处理,那么线程可能一直无限制的等待下去,就算那些正在占用资源的线程死锁了,正在等待的那些资源还是会继续等待,但是ReentrantLock可以选择放弃等待
  3. condition和lock配合使用,以获得最大的性能

行文至此,对于如果选择java中的锁,笔者有以下建议:

  • 1.如果没有特殊的需求,建议使用synchronized,因为操作简单,便捷,不需要额外进行锁的释放。鉴于JDK1.8中的ConcurrentHashMap也使用了CAS+synchronized的方式替换了老版本中使用分段锁(ReentrantLock)的方式,可以得知,JVM中对synchronized的性能做了比较好的优化。
  • 2.如果代码中有特殊的需求,建议使用Lock。例如并发量比较高,且有些操作比较耗时,则可以使用支持中断的所获取方式;如果对于锁的获取,讲究先来后到的顺序则可以使用公平锁;另外对于多个变量的锁保护可以通过lock中提供的condition对象来和lock配合使用,获取最大的性能。
更多原创好文,请关注「逅弈逐码」

相关文章

网友评论

      本文标题:深入理解java中的锁

      本文链接:https://www.haomeiwen.com/subject/culnzxtx.html