Java多线程——Lock和Synchronized底层原理比较

Lock与Synchronized主要区别:

  1. synchronized是Java语言的关键字,因此是内置特性,Lock不是Java语言内置的,Lock是一个接口,通过实现类可以实现同步访问。
  2. synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
  3. 在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态。 

Synchronize底层原理:

 线程状态及状态转换:

    当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:

  • Contention List:所有请求锁的线程将被首先放置到该竞争队列
  • Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
  • Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
  • OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程 

下图反映了个状态转换关系:
Java多线程——Lock和Synchronized底层原理比较_第1张图片

  • EntryList与ContentionList逻辑上同属等待队列,ContentionList会被线程并发访问,为了降低对ContentionList队尾的争用,而建立EntryList。
  • Owner线程在unlock时会从ContentionList中迁移线程到EntryList,并会指定EntryList中的某个线程(一般为Head)为Ready(OnDeck)线程。Owner线程并不是把锁传递给OnDeck线程,只是把竞争锁的权利交给OnDeck,OnDeck线程需要重新竞争锁。这样做虽然牺牲了一定的公平性,但极大的提高了整体吞吐量,在Hotspot中把OnDeck的选择行为称之为“竞争切换”。 
  • OnDeck线程获得锁后即变为owner线程,无法获得锁则会依然留在EntryList中,考虑到公平性,在EntryList中的位置不发生变化(依然在队头)。如果Owner线程被wait方法阻塞,则转移到WaitSet队列;如果在某个时刻被notify/notifyAll唤醒,则再次转移到EntryList。

Lock工作原理:

Lock基本框架结构:

Java多线程——Lock和Synchronized底层原理比较_第2张图片

这里以ReentrantLock为例介绍lock工作流程。

ReentrantLock:

Java多线程——Lock和Synchronized底层原理比较_第3张图片

ReentrantLock内定义了三个内部类:

  • abstract static class Sync extends AbstractQueuedSynchronizer    (核心内部类)
  • static final class NonfairSync extends Sync  (非公平锁)
  • static final class FairSync extends Sync     (公平锁)

ReentrantLock两个构造方法的源码:

public ReentrantLock() {
    sync = new NonfairSync();
}

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

无参构造默认创建的事非公平锁,有参构造可以选择创建公平锁或非公平锁。

lock()方法:

public void lock() {
        sync.lock();
    }

Sync类中的lock()方法是抽象方法,方法的实现在它的两个子类NonfairSync 和 FairSync中,下面以NonefairSync中的lock()方法进行解析:

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

        final void lock() {
            //当有线程竞争锁时,当前线程会首先尝试获得锁而不是在队列中进行排队等候
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

由lock()方法的源码可以看出,当有线程竞争锁时,当前线程会首先尝试获得锁而不是在队列中进行排队等候,这对于那些已经在队列中排队的线程来说显得不公平,也就是非公平锁。刚来竞争的线程首先会通过CAS设置状态,如果设置成功那么直接获取锁,执行临界区的代码,反之调用acquire(1)进入同步队列中。如果已经存在Running线程,那么CAS肯定会失败,则新的竞争线程会通过CAS的方式被追加到队尾。

这里的关键是acquire(1) 方法:

/**
*在排除模式下获取,忽略中断。通过至少调用一次{@link #tryAcquire}来实现,成功后返回。
*否则,线程将排队,可能反复阻塞和解除阻塞,调用{@link#tryAcquire}直到成功。
*此方法可用于实现方法{@link Lock#lock}.
*/
public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
}

acquire方法的作用是完成同步状态的获取,构造用于放入队列中的节点(可以理解为线程任务),加入到队列中,单个节点自己自旋用于检查目前队列中的状况以及当前节点或者是线程阻塞。该方法中又包含三个主要方法:tryAcquire、addWaiter、acquireQueue。 而selfInterrupt方法是在出现异常情况时中断当前线程。

tryAcquire方法,是调用nonfairTryAcquire方法:

protected final boolean tryAcquire(int acquires) {
     return nonfairTryAcquire(acquires);
}

nonefairTryAcquire方法是NonefairSync的父类Sync中的方法:

    /**
     *执行块tryLock。
     *tryAcquire在子类中实现,但两者都需要对trylock方法进行不公平的尝试。
     */   
      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;
        }

1、 该方法会首先判断当前状态,c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。 
            2、 如果c==0,则通过CAS设置该状态值为acquires=1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁,这也是为什么要一个lock对应这个锁对象一个unlock的原因。
           3、如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很       显然这个Running线程并未进入等待队列。
           4、如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非        CAS,也就是说这段代码实现了偏向锁的功能。 

addWaiter方法:

    /**
     * 为当前线程和给定模式创建和排队节点。 
     * @param mode Node.EXCLUSIVE 独占锁, Node.SHARED 共享锁
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

addWaiter方法把当前无法获得锁的线程包装为一个Node添加到队尾,并返回包装后的Node。追加到队尾的动作分两步: 
 1、如果当前队尾已经存在(tail!=null),则使用CAS通过compareAndSetTail方法把当前线程更新为Tail 。
 2、如果当前Tail为null或则线程调用CAS设置队尾失败,则通过enq方法继续设置Tail   

enq方法源码:

     /**
     * 将节点插入队列,必要时初始化
     * @param node the node to insert
     * @return node 的前一节点
     */
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

循环调用CAS,即使有高并发的场景,无限循环将会最终成功把当前线程追加到队尾(或设置队头)

acquireQueue方法:

    /**
     * 获取队列中已存在线程的独占不可中断模式。用于条件等待方法以及获取。
     * @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();
                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);
        }
    }

acquireQueued的主要作用是把已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回。

如果p == head && tryAcquire(arg)条件不满足好像陷入死循环,但是parkAndCheckInterrupt方法会把当前线程挂起,从而阻塞住线程的调用栈。 也就是说,获取锁不成功的线程或被阻塞在parkAndCheckInterrupt方法处,只有当线程被解锁才会继续向下执行。从无限循环的代码可以看出,并不是得到释放锁的线程一定能获得锁,必须调用tryAccquire重新竞争,因为锁是非公平的,有可能被新加入的线程获得,从而导致刚被唤醒的线程再次被阻塞。

unLock()解锁:

    public void unlock() {
        sync.release(1);
    }

release()是抽象类 AbstractQueuedSynchronizer的方法:

    /**
     * 释放独占锁。如果{@link #tryRelease}返回true,则通过解阻塞一个或多个线程来实现。
     * 此方法可用于实现方法{@link Lock#unlock}。
     * @param arg 此值被传递给{@link #tryRelease},但在其他情况下未被解释,可以表示任何内容
     */
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                //唤醒node的后继节点(如果存在的话)。
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

成功释放锁,则唤醒队列第一个线程(Head),返回true,否则返回false。而释放锁的操作是由tryRelease()完成

AbstractQueuedSynchronizer类中的tryRelease()方法被子类Sync重写了,这里实际调用的事Sync中的tryRelease()方法

        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;
        }

如果线程多次锁定,则进行多次释放,直至status==0则真正释放锁,所谓释放锁即设置status为0,因为无竞争所以没有使用CAS。

简述总结:

    总体来讲线程获取锁要经历以下过程(非公平):
    1、调用lock方法,会先进行cas操作看下可否设置同步状态1成功,如果成功执行临界区代码
    2、如果不成功获取同步状态,如果状态是0那么cas设置为1.
    3、如果同步状态既不是0也不是自身线程持有会把当前线程构造成一个节点。
    4、把当前线程节点CAS的方式放入队列中,行为上线程阻塞,内部自旋获取状态。
    5、线程释放锁,唤醒队列第一个节点,参与竞争。重复上述。

 

参考文档:https://blog.csdn.net/liyantianmin/article/details/54673109

你可能感兴趣的:(java多线程)