Java架构师学习之路之并发编程四:Java同步器之synchronized&Lock&AQS(下)

3 篇文章 0 订阅

synchronized

1. synchronized基础回顾

经过上一章的学习,我们知道了synchronized锁有以下特性:

  1. 实现了可见性和原子性
  2. 通过锁对象的对象头记录锁和当前占有锁线程的相关信息
  3. 翻译为字节码文件后会看到monitorenter和monitorexit两个指令
  4. JDK1.8以后,synchronized锁会进行:无锁(01)->偏向锁(01)->轻量级锁(00)->重量级锁(10) 这样的升级
  5. JIT会对锁进行粗化和消除优化
  6. 锁的膨胀升级不可逆

在本章,将会继续学习synchronized锁膨胀升级的具体过程,以及JUC并发包下的锁的实现。

2. synchronized膨胀升级流程

阶段一:
无锁 >> 偏向锁:
当一段时间内只有一个线程竞争锁时,为了避免重复消耗资源竞争锁,因此会在锁对象头的mark word中直接记录当前线程ID,并且在线程获取锁时判断当前线程ID是否和锁头中记录的ID相同。

阶段二:
偏向锁 >> 轻量级锁:
当锁被一个线程(t1)获取后,该锁升级为偏向锁。但是一旦有其他线程(t2)尝试获取锁,即锁头中记录的threadId(t1)不等于当前线程Id(t2)时,该线程获取锁失败。此时t2将会自旋几次,尝试CAS替换threadId。如果替换成功,则继续以偏向锁执行。若自旋几次后仍然获取失败,则等到t1执行到安全点后,暂停t1,将检查t1是否已经执行完同步块,如果同步块执行完毕,则将锁头中的线程信息置空,并恢复t1的执行,由t2继续获取锁。如果同步块未执行完毕,则先将锁头markword中保存的线程信息和锁状态copy到当前持有锁的线程(t1)栈中,同时markword中只保存指向该copy副本的地址。并将线程t1恢复执行。同时t2线程也会把markword信息copy到自己的线程栈,并尝试CAS修改markword的指针。

阶段三:
轻量级锁 >> 重量级锁
在阶段二中,t2线程在多次尝试修改markword指针失败后,轻量级锁升级为重量级锁,同时t2线程被挂起。假如t1在升级为重量级锁后才执行完毕,会尝试CAS修改markword指针,但是此时由于是重量级锁,修改会失败,失败后将锁释放,唤醒阻塞的线程,并开始新一轮竞争。重量级锁的竞争是依赖于申请操作系统的mutex互斥量实现的。

小知识
CAS操作的基准次数可以通过设置JVM参数进行修改。默认10次。
同时JVM对CAS操作次数进行了优化,会基于上次操作消耗的次数会动态修改CAS次数。
假如线程t1在第一次获取锁使用了5次机会,则认为该线程获取锁的几率高,即使第二次获取锁使用了10次机会都没有成功,但是JVM会允许t1多自旋可能3-5次。相反则减少次数。

JUC下的Lock对象和AQS

1. 什么是JUC

JUC全称:java.util.concurrent,即并发编程包。该包下的大部分实现都是由Doug Lee编写的(yyds!)。

2. 什么是Lock对象,如何使用

Lock对象是JUC包下的一个接口,是一组规范。常用的实现类有:

  1. ReentrantLock
  2. ReentrantReadWriteLock.ReadLock
  3. ReentrantReadWriteLock.WriteLock

    这些锁都是基于AQS实现。
    使用方式如下:
        Lock lock = new ReentrantLock();
        lock.lock();
        // 业务代码...
        lock.unlock();

3. AQS是什么

AQS全称:AbstractQueuedSynchronizer,是一种队列同步框架,内部使用**同步队列、条件队列、共享和独占状态**。

4. Lock实现原理(NonFairLock为例)

咱们已经知道了lock的使用方法,加锁只需要执行一个lock()方法,解锁只需要执行unlock()方法。那么其实现原理可以通过这两个方法去查看。
首先咱们进入到ReentrantLock类中:

public class ReentrantLock implements Lock, java.io.Serializable {
	private final Sync sync;

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

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

咱们可以看到lock方法和unlock方法都使用了sync对象。并且new ReentrantLock()时创建的是NonFairSync对象。
那么咱们看一看NonFairSync类:

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

        final boolean initialTryLock() {
            Thread current = Thread.currentThread();
            if (compareAndSetState(0, 1)) { // first attempt is unguarded
                setExclusiveOwnerThread(current);
                return true;
            } else if (getExclusiveOwnerThread() == current) {
                int c = getState() + 1;
                if (c < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(c);
                return true;
            } else
                return false;
        }

        /**
         * Acquire for non-reentrant cases after initialTryLock prescreen
         */
        protected final boolean tryAcquire(int acquires) {
            if (getState() == 0 && compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
    }

再看看Sync类:

abstract static class Sync extends AbstractQueuedSynchronizer {

	// 指向队列头节点---volatile修饰
    private transient volatile Node head;
	// 指向队列尾节点---volatile修饰
    private transient volatile Node tail;
	// 标识锁被占有/重入的次数---volatile修饰
    private volatile int state;

        final void lock() {
            if (!initialTryLock())
                acquire(1);
        }

}

由此我们可以发现,当我们创建ReentrantLock对象时,空参构造会创建一个非公平锁sync对象。而如果我们使用有参构造并传入true,则会创建公平锁sync对象。而他们的父类sync类则继承于AbstractQueuedSynchronizer。
我们接着查看一下AbstractQueuedSynchronizer类:(注意该类还继承于AbstractOwnableSynchronizer类)


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

	static final class Node {

		// 共享节点
        static final Node SHARED = new Node();
		// 独占节点
        static final Node EXCLUSIVE = null;
		// 结束状态,标识该线程已经结束,不需要竞争锁
        static final int CANCELLED =  1;
		// 标识线程可以被唤醒参与锁的竞争
        static final int SIGNAL    = -1;
		// 标识线程是独享状态,进入条件队列
        static final int CONDITION = -2;
        // 标识线程是共享状态,进入同步队列
        static final int PROPAGATE = -3;
        // 线程的状态,也可以理解为信号量,上述4个常量值将被记录在该字段中---volatile修饰
        volatile int waitStatus;
        // 上一个线程
        volatile Node prev;---volatile修饰
        // 下一个线程
        volatile Node next;---volatile修饰
		// 下一个等待的线程(条件队列)
        Node nextWaiter;
    }
}

在AbstractQueuedSynchronizer类中,我们可以看到还有个叫Node的内部类,这个内部类就是组成队列的节点。其中prev和next代表的是同步队列的上一个节点和下一个节点,其类似于双向链表。nextWaiter代表的是条件队列的下一个线程,类似于单向链表。并且每一个节点中都包含了一个waitStatus,这个waitStatus意味着当前节点中存储的线程的信号量,包含了1、-1、-2、-3四种状态。
说完这些基本属性之后,我们再回到方法调用上进行分析:
以NonFairSync为例,调用的是下列lock方法:

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

该方法中,首先进行了compareAndSetState操作,这就是CAS操作,内部调用了Unsafe魔术类,最终通过硬件实现。该操作将尝试将Sync对象中的state加1。
如果替换成功,则代表竞争到锁了,接下来会执行一个方法:

setExclusiveOwnerThread(Thread.currentThread());

该方法从名字上可以看到,是为了给当前sync对象设置独占线程。该独占线程字段在AbstractOwnableSynchronizer类中。

如果没有竞争到锁,将会执行一个重点方法:

            else
                acquire(1);

一起来看下acquire(1)的源码:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

首先进行了!tryAcquire(1)的判断,由于我们实际的对象是NonFairSync对象,所以去该类中找到tryAcquire的实现:

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

然后找到nonfairTryAcquire方法:

	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时,意味着该锁没有被占用,因此,线程将尝试CAS将0更新为1,成功后设置当前线程为独占线程。
state != 0时,将首先判断当前线程和独占线程是不是同一个线程,如果是,则进行重入,直接set方法将原来的state替换为nextc,而nextc = c + 1。最终返回一个true表示获取锁成功,返回false标识获取锁失败。

我们再重新回到这段代码

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我们仅仅执行完第一个判断对不对,这下咱们知道了,!tryAcquire是为了尝试获取锁的,因此这里尝试获取锁失败后(返回false),会执行第二个判断:

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

在这里我们首先需要看一下队列的初始状态:
在这里插入图片描述
即使是空队列,也是有head和tail指针的,但是都指向一个null节点。

addWaiter(Node.EXCLUSIVE)方法中,是如下操作:

  1. Node node = new Node(Thread.currentThread(), mode);创建一个node,并且该node的nextWaiter为null。
  2. Node pred = tail;获取tail指针的指向赋值给pred,此时pred为null。
  3. if (pred != null)判断,如果为pred == null则执行enq(node)。并返回node节点。其实就是尝试将节点添加到头部或者尾部。
    enq(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;
                }
            }
        }
    }
  1. 如果if (pred != null)为ture,则执行下面的逻辑:
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }

其实就是前面有节点了,先将当前节点的prev指向前一个节点,再将当前节点原子替换到尾部,最后将前一个节点的next指向当前节点,完成双向指向。

我们看看看enq中的死循环debug:

首先是第一次循环
在这里插入图片描述
此时发现tail、t、head、node.prev、node.next都是null,且当前节点唯一标志为:Node@1372
此时将走第一个if,来看看第一个循环结束后是什么样的:
在这里插入图片描述
此时发现,tail和head都已经有值了,且都是:Node@1376

接下来进入第二次循环:
在这里插入图片描述
此时tail和t都有值了,都指向Node@1376。我们再走进else代码块中并且断点到return语句:
在这里插入图片描述
此时我们可以看到,传进来的Node@1372的pre指向了新创建的Node@1376,同时1376的next指向了1372,我们的双向链表就完成了。同时还需要注意的是tail和head两个指针,head指向了新创建的空节点Node@1376tail指向了当前节点Node@1372
也就是说,即使只有一个节点加入队列,在最终完成后,至少拥有两个节点。
该方法返回的是head指向的头节点。

用图来表示一下:
第一次for循环之前(啥也没有):
在这里插入图片描述
第一次for循环后:
在这里插入图片描述
第二次for循环,当前节点准备插入
在这里插入图片描述
第二次for循环结束:
在这里插入图片描述
此时返回head指向的节点Node@1376,但是在我们调用enq方法时并没有使用Node@1376,所以最终返回的是当前节点。

看完了addWaiter的实现后,我们一起看看acquireQueued()的实现:
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)的内部实现是:

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

这里我们最容易看到的就是一个死循环。接下来一步步分析:

  1. final Node p = node.predecessor();中,返回的是prev,也就是前一个节点。
  2. 第一个if中,如果prev != head则直接抛出异常。所以一般情况下,这里是相等的(我也没理解哪里会导致不相等…)。
  3. 紧接着进行tryAcquire(arg)的操作,这里其实又在尝试获取锁了。
  4. 第3步获取失败后将进入shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()的判断,当两个判断都ture的情况下,会对线程进行interrupt的操作,暂停线程。第一个判断里,会先判断prev的waitStatus,如果是signal(也就是-1)的情况下直接reture true,因为在当前线程值钱还有线程需要唤醒。接下来判断prev的waitStatus是不是等于取消状态(>0),如果是取消状态,则表明上一个线程执行完了,此时将prev从队列中剔除,并返回false。如果是-2和-3的状态,则执行compareAndSetWaitStatus(pred, ws, Node.SIGNAL);将上一个线程设定为可以被唤醒的状态并返回false。然后当第一个判断返回ture后,进行parkAndCheckInterrupt()的判断。在该方法里,进行interrupt操作并检查是否interrupt成功,成功则返回trure。

这就是非公平锁lock()的全部过程。

unlock的过程等有空分析了咱们再补上。

这一章相对比较难,我分析了挺久时间,也在head和tail指针那里迷茫了很久,如果大家有什么不同的意见,或者文章中哪里写的不正确、有遗漏或者不清楚,请各位大佬指正~~

下一章: Java架构师学习之路之并发编程五:Tools与CountDownLatch与Semaphore原理

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值