【Android每日一问】Lock锁+CAS+与Synchronized比较

242 阅读14分钟

什么是Lock锁?

Lock()是一个接口,是Java中的更加轻量级,更加灵活的,甚至可以自定义的锁,通过实现这个接口,重写它里面的 lock() ,lockInterruptibly() , tryLock() , tryLock(long time, TimeUnit unit) , unlock() , newCondition() 这几个方法,可以实现自定义锁。

Lock源码注解

* {@code Lock} implementations provide more extensive locking
 * operations than can be obtained using {@code synchronized} methods
 * and statements.  They allow more flexible structuring, may have
 * quite different properties, and may support multiple associated
 * {@link Condition} objects.
 * A lock is a tool for controlling access to a shared resource by
 * multiple threads. Commonly, a lock provides exclusive access to a
 * shared resource: only one thread at a time can acquire the lock and
 * all access to the shared resource requires that the lock be
 * acquired first. However, some locks may allow concurrent access to
 * a shared resource, such as the read lock of a {@link ReadWriteLock}.
 * The use of {@code synchronized} methods or statements provides
 * access to the implicit monitor lock associated with every object, but
 * forces all lock acquisition and release to occur in a block-structured way:
 * when multiple locks are acquired they must be released in the opposite
 * order, and all locks must be released in the same lexical scope in which
 * they were acquired.
 * While the scoping mechanism for {@code synchronized} methods
 * and statements makes it much easier to program with monitor locks,
 * and helps avoid many common programming errors involving locks,
 * there are occasions where you need to work with locks in a more
 * flexible way. For example, some algorithms for traversing
 * concurrently accessed data structures require the use of
 * "hand-over-hand" or "chain locking": you
 * acquire the lock of node A, then node B, then release A and acquire
 * C, then release B and acquire D and so on.  Implementations of the
 * {@code Lock} interface enable the use of such techniques by
 * allowing a lock to be acquired and released in different scopes,
 * and allowing multiple locks to be acquired and released in any
 * order.
 * With this increased flexibility comes additional
 * responsibility. The absence of block-structured locking removes the
 * automatic release of locks that occurs with {@code synchronized}
 * methods and statements. In most cases, the following idiom
 * should be used:
 * When locking and unlocking occur in different scopes, care must be
 * taken to ensure that all code that is executed while the lock is
 * held is protected by try-finally or try-catch to ensure that the
 * lock is released when necessary.
 *
 * <p>{@code Lock} implementations provide additional functionality
 * over the use of {@code synchronized} methods and statements by
 * providing a non-blocking attempt to acquire a lock ({@link
 * #tryLock()}), an attempt to acquire the lock that can be
 * interrupted ({@link #lockInterruptibly}, and an attempt to acquire
 * the lock that can timeout ({@link #tryLock(long, TimeUnit)}).

CSDN博主解析:
Lock接口提供了更具扩展性的锁,允许更加灵活的结构,更多种类的属性,且不使用synchronized关键字声明和相关的方法。
Lock接口是一种对多线程共享资源访问的控制工具。一次只能有一个线程获取锁,其余线程访问共享资源都会要求先获得锁,但是也会有一些锁允许并发访问资源的操作,比如读锁。
Synchronized方法或者声明语句提供了针对每个对象的隐式监视器,但是获取锁和释放锁操作都是以块状粒度。当获取锁的时候都要求必须先执行与获取锁相反的释放指令。而且全部的锁都必须在执行完他们锁的语义范围之后释放。
Synchronized方法的作用域机制,并且通过声明语句使得实现监视器锁编程更加简单。
同时有利于避免许多关于锁的常见编程错误。
但是有些时候你会需要在一个更加灵活的方式下使用锁,例如一些遍历算法,并发访问的数据结构。比如:先获取节点A的锁,再获取节点B的锁,然后释放A获取C,释放B获取D,以此类推。
那么这种灵活的锁就可以通过Lock()来实现,在不同的逻辑范围内通过不同的指令条件来获取和释放锁,
随着出现越来越多的这种对灵活性要求的场景,无用的块状结构锁将被淘汰删除。自动释放和加锁都应该采用方法声明,在更多情况下,Lock()应该使用:
Lock() l = (你自定义的实现了Lock接口的锁) …;
l.lock() ; 加锁
try(){
} finally {
l.unlock(); 释放锁
}
当锁定和解锁在不同的范围发生时,必须确保用try catch捕抓锁定的代码块执行时候出现的异常,确保必要时释放锁不至于造成死锁。
Lock() 提供了新的方法 tryLock() 是尝试获取锁,以及tryLock(long,TimeUnit)来设置锁的固定超时时间。
Lock() 接口的实现类提供的行为和语义都与隐式监视器锁有很大的不同,例如可以保证排序,不可重入或者死锁检测。
注意Lock()实例只是普通对象,实例的锁监视器锁与Lock实例没有特别的关系,除了在它们自己的实现中,建议不要在它们自己的实例中使用Lock这样的实例来避免混淆。
除非另有说明,否则传递的任何参数都要抛出空指针异常(这也就是必须用try catch 捕获的原因之一)。

使用Lock()需要注意的有以下几点:

  • 一定要记得释放,防止死锁。
  • 加异常捕获机制
  • 加超时机制等等

Lock()中的公平锁和非公平锁

公平锁:大家顺序加锁

非公平锁:共同竞争,谁抢到就是谁的

由于Lock()是一个接口,所以我们可以看一下它的实现类ReentrantLock()

ReentrantLock()可重入锁

ReentrantLock名称直译就是可重入锁,它的可重入,目的是为了解决递归调用的时候,同一个线程获取锁,明明是同一个线程,却还是要不断等待锁释放再加锁的死锁和低效率行为。Synchronize中也有可重入的引入,一样是为了解决这问题。原理就是拿当前占据了锁的node节点中的Thread去跟等待获取锁的Thread比较一下,如果相等,则直接在标志位status++;递归了多少次,就++ 多少,执行完一次,status–;直到status = 0 ,才证明全部执行完毕,让出锁,而无需阻塞添加到等待队列中等待

public ReentrantLock() {
        sync = new NonfairSync();
    }
public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

它有两个构造方法可以调用,boolean fair参数决定了它是否是公平锁。

下面我们看一下它是怎么样实现线程安全的(公平):

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.
         */
        // Android-removed: @ReservedStackAccess from OpenJDK 9, not available on Android.
        // @ReservedStackAccess
        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;
        }
    }

FairSync是它的一个静态内部类。 可以看到如果是公平锁的话会判断当前线程是否还有前面的node节点需要获取锁,也就是排队,如果有,则直接放弃获取锁,如果没有,则自己拿锁。 compareAndSetState(0, acquires)state==0代表锁没有被线程占用,获取后会更新为1.

如果是非公平锁:

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        // Android-removed: @ReservedStackAccess from OpenJDK 9, not available on Android.
        // @ReservedStackAccess
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
  • 非公平锁的状态下,直接让所有线程去争抢,通过setExclusiveOwnerThread方法传入一个Thread,谁抢到谁用,其他的继续在双向链表中等待。

底层实现是通过一个AbstractQueuedSynchronizer抽象队列,通过类似链表的结构让多线程操作在这里排队,依次拿到线程任务出去执行,当调用lock.unlock()后激活后续任务执行。这个抽象队列叫做CLH queue

这里看一下Node的数据结构是什么:

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled. */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking. */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition. */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate.
         */
        static final int PROPAGATE = -3;
        
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

        /**
         * Returns previous node, or throws NullPointerException if null.
         * Use when predecessor cannot be null.  The null check could
         * be elided, but is present to help the VM.
         *
         * @return the predecessor of this node
         */
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        /** Establishes initial head or SHARED marker. */
        Node() {}

        /** Constructor used by addWaiter. */
        Node(Node nextWaiter) {
            this.nextWaiter = nextWaiter;
            U.putObject(this, THREAD, Thread.currentThread());
        }

        /** Constructor used by addConditionWaiter. */
        Node(int waitStatus) {
            U.putInt(this, WAITSTATUS, waitStatus);
            U.putObject(this, THREAD, Thread.currentThread());
        }

        /** CASes waitStatus field. */
        final boolean compareAndSetWaitStatus(int expect, int update) {
            return U.compareAndSwapInt(this, WAITSTATUS, expect, update);
        }

        /** CASes next field. */
        final boolean compareAndSetNext(Node expect, Node update) {
            return U.compareAndSwapObject(this, NEXT, expect, update);
        }

        private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
        private static final long NEXT;
        static final long PREV;
        private static final long THREAD;
        private static final long WAITSTATUS;
        static {
            try {
                NEXT = U.objectFieldOffset
                    (Node.class.getDeclaredField("next"));
                PREV = U.objectFieldOffset
                    (Node.class.getDeclaredField("prev"));
                THREAD = U.objectFieldOffset
                    (Node.class.getDeclaredField("thread"));
                WAITSTATUS = U.objectFieldOffset
                    (Node.class.getDeclaredField("waitStatus"));
            } catch (ReflectiveOperationException e) {
                throw new Error(e);
            }
        }
    }

从上可知:

  • CLH queue当中存储的是Thread
  • nextWaiter 则是下一个等待执行的线程。
private Node enq(Node node) {
        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                U.putObject(node, Node.PREV, oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return oldTail;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

通过上述方法将新的线程追加到队尾,并返回node。追加的时候调用了U.putObject(node, Node.PREV, oldTail)也就是我们接下来要介绍的CAS。

CAS(Compare and Swap)

  • CAS是基于算法实现的一种乐观锁。是CPU指令级别的原子操作。 在Node的源码结构中我们可以看到,不管是设置前节点,后节点,还是等待操作,都传入了3个参数:
  • this:当前内存值
  • expect:预期值
  • update:更新后的新值

举例如下:

转自网络:
一个自增计数,发生了线程不安全情况。因为会有多个线程拿到旧值同时++,
导致多次操作集成一次,数据统计不正确了。
CAS的解决方法就是,每个线程都会拿到旧值(预期值),
然后++后计算出一个结果值,再把前面拿到的预期值跟内存地址中的值比较,
看预期结果是否等于内存地址中现存的值,如果预期值=内存值,
证明没有其他线程操作过,是安全的,于是把结果值放进去。
如果不等于,就会把现在内存中的值拿出去做预期值,
预期值++当作是结果值,再CAS比较一遍,
这个过程就是CAS的自旋,这样就可以保证线程安全。

如上所说分下面几步:CAS的自旋

  1. 每个线程都先拿到旧值当作预期值
  2. 本线程进行++运算得到新值
  3. 拿之前拿到的预期值和内存地址的值比较,如果一样说明没有其他线程操作,安全,新值放入
  4. 如果不等于,再次拿内存值作为预期值
  5. 预期值++再次得到新值
  6. 再次CAS比较

CAS著名的ABA问题:

问题如下:

  • 线程A对数据进行了两次修改:将初始值1改为2又再次改为1

  • 线程B通过CAS比较发现数据一致,更新了新值,这样就导致A的操作被覆盖掉 以上就导致了问题的发生。在一些对数据准确性要求较高的场景是有问题的。例如转账等。

解决CAS的ABA问题:

新增一个参数叫做版本号

  • 取数据时除了数据顺便拿一个现存版本号,然后版本好进行自增,证明做过了操作。
  • 自增的版本号作为预期新值版本号,在比较的时候顺便把版本号也比较一下,版本号不对证明发生了线程安全问题,再次取出现存版本号并自增,再做一次CAS,通过后设置新值。

综上所述:

  • 相比起Synchronize,CAS提升的效率是百倍算的,它把锁的粒度缩小到了极致,对性能提升是巨大的。

CAS与Synchronized的使用情景:

  • 对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

  • 对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

Synchronized 和 ReenTrantLock 的对比

  • 两者都是可重入锁 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

  • synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

  • ReenTrantLock 比 synchronized 增加了一些高级功能

  1. 等待可中断
  • ReenTrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  1. 可实现公平锁
  • ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  1. 可实现选择性通知(锁可以绑定多个条件)
  • synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
  1. 性能已不是选择标准
  • 在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量岁线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步,优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作。