腾讯云后端15连问!(二)

chujichenxuyuan
发布于 2022-6-29 13:51
浏览
0收藏

4. Future 实现阻塞等待获取结果的原理?


Future.get()用于异步结果的获取。它是阻塞的,背后原理是什么呢?

 

我们可以看下FutureTask的类结构图:腾讯云后端15连问!(二)-鸿蒙开发者社区FutureTask实现了RunnableFuture接口,RunnableFuture继承了Runnable和Future这两个接口, 对于Runnable,我们太熟悉了, 那么Future呢?

 

Future 表示一个任务的生命周期,并提供了相应的方法来判断是否已经完成或取消,以及获取任务的结果和取消任务等。

public interface Future<V> {

    boolean cancel(boolean mayInterruptIfRunning);
    //Future 是否被取消
    boolean isCancelled();
    //当前 Future 是否已结束
    boolean isDone();
    //或取Future的结果值。如果当前 Future 还没有结束,当前线程阻塞等待,
    V get() throws InterruptedException, ExecutionException;
    //获取 Future 的结果值。与 get()一样,不过多了超时时间设置
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask 就是Runnable和Future的结合体,我们可以把Runnable看作生产者, Future 看作消费者。而FutureTask 是被这两者共享的,生产者运行run方法计算结果,消费者通过get方法获取结果。

 

生产者消费者模式,如果生产者数据还没准备的时候,消费者会被阻塞。当生产者数据准备好了以后会唤醒消费者继续执行。我们来看下FutureTask内部是如何实现的。

 

FutureTask内部维护了任务状态state

//NEW 新建状态,表示FutureTask新建还没开始执行
private static final int NEW          = 0;
//完成状态,表示FutureTask
private static final int COMPLETING   = 1;
//任务正常完成,没有发生异常
private static final int NORMAL       = 2;
//发生异常
private static final int EXCEPTIONAL  = 3;
//取消任务
private static final int CANCELLED    = 4;
//发起中断请求
private static final int INTERRUPTING = 5;
//中断请求完成
private static final int INTERRUPTED  = 6;

生产者run方法:

public void run() {
        // 如果状态state不是 NEW,或者设置 runner 值失败,直接返回
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //调用callable的call方法,获取结果
                    result = c.call();
                    //运行成功
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    //运行不成功
                    ran = false;
                    //设置异常
                    setException(ex);
                }
                //运行成功设置返回结果
                if (ran)
                    set(result);
            }
        } finally {
            runner = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

消费者的get方法

 public V get() throws InterruptedException, ExecutionException {
     int s = state;
     //如果状态小于等于 COMPLETING,表示 FutureTask 任务还没有完成, 则调用awaitDone让当前线程等待。
     if (s <= COMPLETING)
         s = awaitDone(false, 0L);
     return report(s);
 }

awaitDone做了什么事情呢?

 private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            // 如果当前线程是中断标记,则  
            if (Thread.interrupted()) {
                //那么从列表中移除节点 q,并抛出 InterruptedException 异常
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            //如果状态已经完成,表示FutureTask任务已结束
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                //返回
                return s;
            }
            // 表示还有一些后序操作没有完成,那么当前线程让出执行权
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
            //将当前线程阻塞等待
            else if (q == null)
                q = new WaitNode();
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            //timed 为 true 表示需要设置超时                                        
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                //让当前线程等待 nanos 时间
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

当然,面试的时候,不一定要讲到源码这么细,只需要讲个大概思路就好啦。

 

5. ReentrantLock和Synchronized的区别?Synchronized 原理?


ReentrantLock和 Synchronized 的区别?

 

 •  Synchronized是依赖于JVM实现的,而ReenTrantLock是API实现的。
 •  在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者性能就差不多了。
 •  Synchronized的使用比较方便简洁,它由编译器去保证锁的加锁和释放。而ReenTrantLock需要手工声明来加锁和释放锁,最好在finally中声明释放锁。
 •  ReentrantLock可以指定是公平锁还是⾮公平锁。⽽synchronized只能是⾮公平锁。
 •  ReentrantLock可响应中断、可轮回,而Synchronized是不可以响应中断的,


至于Synchronized的原理,大家可以看我这篇文章哈

 

Synchronized解析——如果你愿意一层一层剥开我的心

 

6. 聊聊AOS?ReentrantLock的实现原理?


AQS(抽象同步队列)的核心回答要点就是:

 

 •  state 状态的维护。
 •  CLH队列
 •  ConditionObject通知
 •  模板方法设计模式
 •  独占与共享模式。
 •  自定义同步器。


大家可以看下我之前这篇文章哈:AQS解析与实战

 

大家综合ReentrantLock的功能,比如可重入,公平锁,非公平锁等,与AQS结合一起讲就好啦。

 

7. 乐观锁和悲观锁, 让你来写你怎么实现?


悲观锁:

 

悲观锁她专一且缺乏安全感了,她的心只属于当前线程,每时每刻都担心着它心爱的数据可能被别的线程修改。因此一个线程拥有(获得)悲观锁后,其他任何线程都不能对数据进行修改啦,只能等待锁被释放才可以执行。腾讯云后端15连问!(二)-鸿蒙开发者社区 •  SQL语句select ...for update就是悲观锁的一种实现
 •  还有Java的synchronized关键字也是悲观锁的一种体现


乐观锁:

 

乐观锁的很乐观,它认为数据的变动不会太频繁,操作时一般都不会产生并发问题。因此,它不会上锁,只是在更新数据时,再去判断其他线程在这之前有没有对数据进行过修改。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。腾讯云后端15连问!(二)-鸿蒙开发者社区之前业务上使用过CAS解决并发问题,大家有兴趣可以看一下哈:

 

 •  CAS乐观锁解决并发问题的一次实践


8. Paxos 协议了解?工作流程是怎么样的?


8.1 为什么需要Paxos算法?


当前我们应用都是集群部署的,要求所有机器状态一致。假设当前有两台机器A和B,A要把状态修改为a,B要把状态修改为b,那么应该听谁的呢?这时候可以像2PC一样,引入一个协调者,谁最先到就听谁的。腾讯云后端15连问!(二)-鸿蒙开发者社区这里有个问题,就是协调者是单节点,如果它挂了呢。因为可以引入多个协调者腾讯云后端15连问!(二)-鸿蒙开发者社区但是这么多协调者,应该听谁的呢?

 

引入Paxos算法解决这个问题,Paxos算法是一种基于消息传递的分布式一致性算法。


8.2  Paxos的角色


Paxos涉及三种角色,分别是Proposer、Accecptor 、Learners。

 

 •  Proposer:它可以提出提案 (Proposal),提案信息包括提案编号和提案值
 •  Acceptor:接受接受(accept)提案。一旦接受提案,提案里面的提案值(可以用V表示)就被选定了。
 •  Learner: 哪个提案被选定了, Learner就学习这个被选择的提案


一个进程可能是Proposer,也可能是Acceptor,也可能是Learner。


8.2 Paxos算法推导过程


一致性算法需要前置条件

 

 •  在这些被提出的提案中,只有一个会被选定
 •  如果没有提案被提出,就不应该有被选定的提案 -当提案被选定后,learner可以学习被选中的提案


假设只有一个Acceptor,只要Acceptor接受它收到的第一个提案,就可以保证只有一个value会被选定。但是这个 Acceptor 宕机,会导致整个系统不可用。腾讯云后端15连问!(二)-鸿蒙开发者社区如果是是多个Proposer和多个Acceptor,如何选定一个提案呢?腾讯云后端15连问!(二)-鸿蒙开发者社区我们可以加个约定条件,假设就叫约束P1:一个 Acceptor 必须接受它收到的第一个提案。但是这样还是可能会有问题,如果每个Proposer分别提出不同的value(如下图V1,V2,V3),发给了不同的Acceptor,最后会导致不同的value被选择。腾讯云后端15连问!(二)-鸿蒙开发者社区我们可以给多一个额外的约定P1a:一个提案被选定,需要被半数以上 Acceptor 接受。这跟P1有点矛盾啦,我们可以使用一个全局的编号来标识每一个Acceptor批准的提案,当一个具有某value值的提案被半数以上的Acceptor批准后,我们就认为该value被选定了。即提案P= 提案参数 + 提案值,可以记为【M,V】。

 

现在可以允许多个提案被选定,但必须保证所有被选定的提案都具有相同的value值。要不然又会出现不一致啦。因此可以再加个约束P2:

如果提案 P[M1,V1] 被选定了,那么所有比M1编号更高的被选定提案P,其 value 的值也必须是 V1。

一个提案要被选定,至少要被一个 Acceptor 接受,因此我们可以把P2约束改成对Acceptor接受的约束P2a:

如果提案 P[M1,V1] 被接受了,那么所有比M1编号更高的,且被Acceptor接受的P,其值也是 V1。

多提案被选择的问题解决了,但是如果是网络不稳定或者宕机的原因,还是会有问题。腾讯云后端15连问!(二)-鸿蒙开发者社区假设有 5 个 Acceptor。Proposer2 提出 [M1,V1]的提案,Acceptor2~5(半数以上)均接受了该提案,于是对于 Acceptor2~5 和 Proposer2 来讲,它们都认为 V1 被选定。Acceptor1 刚刚从 宕机状态 恢复过来(之前 Acceptor1 没有收到过任何提案),此时 Proposer1 向 Acceptor1 发送了 [M2,V2] 的提案 (V2≠V1且M2>M1)。对于 Acceptor1 来讲,这是它收到的 第一个提案。根据 P1(一个 Acceptor 必须接受它收到的 第一个提案),Acceptor1 必须接受该提案。同时 Acceptor1 认为 V2 被选定。


这就出现了两个问题:

 

 •  Acceptor1 认为V2被选定,Acceptor2~5Proposer2认为V1被选定。出现了不一致。
 •  V1被选定了,但是编号更高的被Acceptor1接受的提案[M2,V2]的 value 为 V2,且 V2≠V1。这就跟 P2a(如果提案 P[M1,V1] 被接受了,那么所有比M1编号更高的,且被Acceptor接受的P,其值也是 V1。)矛盾了。


我们要对P2a约束强化一下得到约束P2b,

如果 P[M1,V1] 被选定后,任何Proposer 产生的 P,其值也是 V1。

对于 P2b 中的描述,如何保证任何Proposer产生的P,其值也是V1 ?只要满足 P2c 即可:

 

对于任意的M和V,如果提案[M,V]被提出,那么肯定存在一个由半数以上的Acceptor组成的集合S,满足以下两个条件 中的任意一个:

要么S中每个Acceptor都没有接受过编号小于M的提案。
要么S中所有Acceptor批准的所有编号小于Mn的提案中,编号最大的那个提案的value值为Vn


8.3 算法流程


8.3.1. Proposer生成提案


 •  Prepare请求
 •  Accept请求


P2c约束基础上,如何生成提案呢?

 

Proposer选择一个新的提案编号N,向 Acceptor 集合 S(数目在半数以上)发送请求,要求 S 中的每一个 Acceptor 做出如下响应:

 

 •  如果 Acceptor 没有接受过提案,则向 Proposer 保证 不再接受编号小于N的提案。
 •  如果 Acceptor 接受过请求,则向 Proposer 返回 已经接受过的编号小于N的编号最大的提案。

 

我们将这个请求称为编号为N的Prepare请求。

 

 •  如果Proposer收到半数以上的Acceptor 响应,则生成编号为N,value 为 V 的提案 [N,V],V 为所有响应中编号最大的提案的value。
 •  如果 Proposer收到的响应中没有提案,那么 value 由 Proposer 自己生成,生成后将此提案发给 S,并期望Acceptor 能接受此提案。

 

我们称这个请求为Accept请求

 

8.3.2 Acceptor接受提案


一个Acceptor可能会受到来自Proposer的两种请求:Prepare请求和Accept请求。Acceptor 什么时候可以响应一个请求呢,它也有个约束:P1b:

一个Acceptor只要尚未响应过任何编号大于N的Prepare请求,那么他就可以接受这个编号为N的提案。

Acceptor收到编号为 N的Prepare 请求,如果在此之前它已经响应过编号大于N的Prepare请求。由约束P1b,该Acceptor不会接受这个编号为N的提案。因此,Acceptor会忽略这个请求。

 

一个 Acceptor 只需记住两点:已接受的编号最大的提案和已响应的请求的最大编号。

 

8.3.3 Paxos算法描述


阶段一:

 

 •  Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。
 •  如果一个Acceptor收到一个编号为N的Prepare请求,且N大于该Acceptor已经响应过的所有Prepare请求的编 号,那么它就会将它已经接受过的编号最大的提案(如果有的话)作为响应反馈给Proposer,同时该Acceptor 承诺不再接受任何编号小于N的提案。


阶段二:

 

 •  如果Proposer收到半数以上Acceptor对其发出的编号为N的Prepare请求的响应,那么它就会发送一个针对 [N,V]提案的Accept请求给半数以上的Acceptor。注意:V就是收到的响应中编号最大的提案的value,如果响应 中不包含任何提案,那么V就由Proposer自己决定。
 •  如果Acceptor收到一个针对编号为N的提案的Accept请求,只要该Acceptor没有对编号大于N的Prepare请求 做出过响应,它就接受该提案。

 

8.3.4 Learner学习被选定的value
 腾讯云后端15连问!(二)-鸿蒙开发者社区

标签
已于2022-6-29 13:51:12修改
收藏
回复
举报
回复