2020后端面试,java篇

2020-03-08  本文已影响0人  润着

2019.12-2020.02后端面试材料分享,架构/中间件篇。

拿到了字节offer,走完了Hello单车和达达的面试流程(没给offer),蚂蚁的前三轮(接了字节Offer,放弃后续流程)。

以下问题汇总在一个类anki的小程序:一进制。默认隐藏答案,思考后再点开对照;根据你反馈的难度,安排复习时间。

"一进制",java篇

-问题1: 边用iterator遍历hashMap,边通过hashMap自身方法修改数据,有什么问题?

-tags: java,hashmap,并发

-解答:
会拋出ConcurrentModificationException

因为HashMap的modCount和Iterator维护的expectedModCount不相同了。正确的做法是只通过HashMap本身或者只通过Iterator去修改数据。


-问题2: 相比Java7,Java8的ConcurrentHashMap做了什么改进?

-tags: java,hashmap,并发

-解答:

Java8中,put方法和remove方法都会通过addCount方法维护Map的size,size方法通过sumCount获取由addCount方法维护的Map的size。

addCount方法统计数值baseCount(正常无并发下的节点数量)和counterCells(并发插入下的节点数量),以精确计算并发读写情况下table中元素的数量。它首先尝试用CAS更新baseCount,成功,如果CAS操作失败,则表示有竞争,有其他线程并发插入,则修改的数量会被记录到CounterCell中。


-问题3: 静态数据、构造函数、代码段、字段,执行的顺序是怎样的?

当创建类的实例时,父类和子类的静态数据、构造函数、代码段、字段,执行的顺序是怎样的?

-tags: java

-解答:
按照先父类后子类、先静态后动态的顺序:


-问题4: 有没有有顺序的 Map 实现类, 如果有, 它们是怎么保证有序的?

-tags: java

-解答:


-问题5: error和exception的区别,CheckedException,RuntimeException的区别

-tags: java

-解答:

额外补充:


-问题6: 自己创建一个java.lang.String对象,会被类加载器加载吗?

在自己的代码中,创建一个java.lang.String对象,会被类加载器加载吗,为什么

-tags: java

-解答:
不可以
启动类加载器(bootstrap)会将Java_Home/lib下面的类库加载到内存中,所以自己创建的java.lang.String对象无法被加载


-问题7: 分代GC中,对象从创建到回收的一般流程是怎样的?

-tags: java

-解答:
HotSpot JVM把年轻代分为了三部分: 1个Eden区和2个Survivor区(分别叫fromto),默认大小比例为8:1。

pic

一般情况下,新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。

因为年轻代中的对象基本都是朝生夕死的(80%以上),所以在年轻代的垃圾回收算法使用的是复制算法,在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”

Full GC 是发生在老年代的垃圾收集动作,采用的是标记-清除算法。该算法会产生内存碎片,此后为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。


-问题8: GC的触发条件有哪些?

-tags: java

-解答:

GC 触发条件


-问题9: 谈谈对Spring框架设计理念的理解

Spring解决了什么关键的问题?它有哪几个核心组件?为什么需要这些组件?它们是如何结合在一起构成Spring的骨骼架构的?

-tags: java,spring

-解答:
Spring框架中的核心组件只有三个:Context ,Core和Beans,它们构建起了整个Spring 的骨骼架构。其中Beans又是核心中的核心。

关键问题

Spring如此流行,是因为解决了一个非常关键的问题:它把对象间的依赖关系转而用配置文件来管理,也就是它的依赖注入机制。而这个注入关系在一个叫IOC 的容器中管理。Spring 正是通过把对象包装在Bean中来达到对这些对象管理以及额外操作的目的。

协同工作

我们把Bean比作一场演出中的演员,那么Context就是舞台背景,Core就是演出的道具。

对Context 来说它就是要发现每个Bean之间的关系,为它们建立这种关系并维护好这种关系。所以Context就是一个 Bean关系的集合,这个关系集合又叫IOC容器。一旦建立起这个IOC容器之后, Spring就可以为你工作了。那么Core组件又有何用武之地呢?其实Core 就是发现,建立和维护Bean之间的关系所需要的一系列工具,从这个角度看Core这个组件叫Util更能让你理解。

IOC容器如何工作

IOC容器实际上就是 Context组件结合其他两个组件共同构建了一个Bean的关系网。网的构建的入口就在AbstractApplicationContext类的refresh方法中,它做了以下事情:


-问题10: 准备用HashMap存1w条数据,构造时传10000还会触发扩容吗?

准备用HashMap存1w条数据,构造时传10000还会触发扩容吗?
如果是准备存1k条,构造时传1000呢?

-tags: java

-解答:
不会
JDK8的源码来说明:

那么


-问题11: Cpu飙高,jstack发现最耗cpu的线程却是waiting状态

通过top -Hp $pid 找到最耗CPU的线程,再jstack, 到输出里查这个线程,发现它却被LockSupport.park挂起了,处于WAITING状态。这是怎么回事?

-tags: java,并发

-解答:
首先,LockSupport.park的注释里明确提到park的线程不会再被CPU调度的。所以可以大胆推断不是线程本身的代码消耗cpu。

那么,最有可能的便是线程的上下文切换。如果不确信它可以占用多少资源,可以做个实验,启动几千个线程,用LockSupport.park()不断挂起这些线程, 再使用LockSupport.unpark(t)不断地唤醒这些线程.,唤醒之后又立马挂起,观察期间cpu的使用情况便可知道。

那么,问题是,具体是什么消耗了cpu呢?从代码调用栈中找答案。

at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)

AQS是很多jdk并发库的底层框架,

在AQS的第2步中, 如果竞争锁失败的话, 是会使用CAS乐观锁的方式添加到队列尾的, 核心代码如下

  /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    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;
                }
            }
        }
    }

在并发量非常高的情况下, 每一次执行compareAndSetTail都失败(即返回false)的话,那么这段代码就相当是一个死循环,消耗cpu就不奇怪了。

更进一步,如果进入临界区后很快就做完了业务逻辑,会导致CLH队列的线程被频繁唤醒,而又由于抢占锁失败频繁地被挂起,也会带来大量的上下文切换, 消耗系统的cpu资源。


-问题12: 诡异的NaN

浮点数NaN(Not-a-Number)有什么“诡异”的特性?

-tags: java

-解答:
先绕圈讲点别的。

在 Java 中,正无穷和负无穷是有确切的值,在内存中分别等同于十六进制整数 0x7F800000 和 0xFF800000。

这个范围之外,即:[0x7F800001, 0x7FFFFFFF] 和 [0xFF800001, 0xFFFFFFFF] 对应的就是NaN

NaN 有一个有趣的特性:除了“!=”始终返回 true 之外,所有其他比较结果都会返回 false。举例来说,“NaN<1.0F”返回 false,而“NaN>=1.0F”同样返回 false。对于任意浮点数 f,不管它是 0 还是 NaN,“f!=NaN”始终会返回 true,而“f==NaN”始终会返回 false。


-问题13: java中byte和long类型变量分别占用多少内存空间

-tags: java

-解答:
boolean、byte、char、short 这四种类型,在栈上占用的空间和 int 是一样的。因此,在 32 位的 HotSpot 中,这些类型在栈上将占用4个字节;而在 64 位的 HotSpot 中,他们将占8个字节。

当然,对于 byte、char 以及 short 这三种类型,它们在堆上占用的空间分别为一字节、两字节,以及两字节,也就是说,跟这些类型的值域相吻合。

无论在哪里,longdouble类型都占用8字节。


-问题14: jvm加载类的过程

-tags: java

-解答:
jvm加载java类就是将字节流文件加入到内存中的过程,分为以下三步:加载链接初始化

加载:查找字节流并且据此创建类的过程,每一种类加载器加载一部分类。

链接:验证、准备、解析

初始化:为标记为常量值的字段(基本类型或字符串且被修饰为final)赋值,以及执行<clinit>方法(其他赋值操作和静态代码块)


-问题15: Java虚拟机调用方法的大致过程

方法是如何被找到和执行的?

-tags: java

-解答:
Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令。这两种指令,均属于 Java 虚拟机中的虚方法调用。这里只解释虚方法的调用 过程。

如果这两种指令所声明的目标方法被标记为 final,那么 Java 虚拟机会采用静态绑定。否则,Java 虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。

而动态绑定又是通过方法表这一数据结构来实现的。方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类非私有的实例方法。

在解析虚方法调用时,Java 虚拟机会纪录下所声明目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。这个过程便是动态绑定。

Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。


-问题16: java反射慢的原因是什么

都说java反射慢,究竟慢在哪了,为什么慢?

-tags: java

-解答:
以使用java反射调用方法为例,一般流程是:

先用Class.forName获取类对象,再用Class.getMethod获取方法,最后执行Method.invoke进行动态调用。

其中,Class.forName会调用本地方法,Class.getMethod则会遍历该类的公有方法。如果没有匹配到,还将遍历父类的公有方法。所以这两个操作都非常费时。

另外,getMethod返回的是结果的一份拷贝。在热点代码中频率使用它或类似的getMethodsgetDeclaredMethods会带来较多的堆空间消耗。

反射调用本身也有较高性能消耗。


-问题17: invokedynamic指令有何特点?

和其它方法调用指令invokestatic,invokespecial,invokevirtual等相比,invokedynamic有何特点?

-tags: java

-解答:
上述指令中,Java 虚拟机明确要求方法调用需要提供目标方法的类名。它们使用上不够灵活,执行效率也不高。
为此,Java 7 引入了一条新的指令 invokedynamic。该指令抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。

其底层依赖方法句柄,这是一个强类型的、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。

方法句柄的权限检查发生在创建过程中,相较于反射调用,节省了调用时反复检查权限的开销。


-问题18: gc的安全点概念

-tags: java

-解答:

什么是安全点

安全点(safepoint)是在代码执行过程中的特殊位置,当线程执行到这些位置时,可以暂停,安全地进行GC,而不会引发混乱。

哪些状态属于安全点?

对线程可能在干的事一个个讨论:

会出现长时间达到不安全点的情况吗?

不会。从第二问的分析看,当在虚拟机掌握范围内时,虚拟机可以方便地进行安全点检测。需要考虑的是执行本地代码即时编译的机器码时。


-问题19: 垃圾回收的基本方式有哪几种,分别有何优缺点

比如其中一种是“清除”

-tags: java

-解答:

一,清除

把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。

缺点是

二,压缩

把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。

缺点是压缩算法有较大性能开销。

三,复制

把内存区域分为两等分,总是把存活对象复制到其中一半,腾空的另一半给新对象用,如此来回倒腾。缺点是堆空间的使用效率极其低下。


-问题20: 新生代和老年代分别有哪些适用的垃圾回收器?

各自有什么特点?

-tags: java

-解答:

新生代

老年代


-问题21: 请描述G1垃圾回收器

-tags: java

-解答:
G1直接将堆分成很多个区域。每个区域都可以充当Eden区Survivor区或者老年代中的一个。

它采用的是标记 - 压缩算法,而且和 CMS 一样都能够在应用程序运行过程中并发地进行垃圾回收。

G1在选择进行垃圾回收的区域时,会优先回收死亡对象较多的区域。


-问题22: synchronized所加的锁可能有几种,轻重排序是怎样的?

-tags: java

-解答:
按照从重到轻的排序,可能是重量级锁轻量级锁偏向锁

重量级锁状态下,加锁失败的线程会被阻塞,唤醒也需要靠操作系统,成本比较高。为了改善成本,虚拟机会在线程进入阻塞状态之前,以及被唤醒后竞争不到锁的情况下,进入自旋状态。通俗地理解,阻塞好比熄火停车,自旋好比怠速停车。

轻量级锁好比深夜的十字路口,四个方向都闪黄灯的情况,车很少,偶尔一两辆,自行观察后通过即可。

偏向锁更进一步,好比能识别救护车的红绿灯,如果匹配到救护车,直接亮绿灯放行。


-问题23: 轻量级锁的加锁和解锁过程是怎样的?

-tags: java

-解答:

加锁

首先在当前线程的当前栈桢中划出一块空间,作为该锁的锁记录,将锁对象的标记字段复制到该锁记录中。然后,虚拟机会尝试用CAS操作替换锁对象的标记字段

解锁


-问题24: 什么是即时编译的分层编译

-tags: java

-解答:
从Java 8开始,JVM默认采用分层编译的方式,分为:

通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。

方法会首先被解释执行,然后被 3 层的 C1 编译,最后被 4 层的 C2 编译。

第3层C1编译中,profile收集的关于分支以及类型的数据,可用来推断程序今后的执行。这些推断会精简代码的控制流以及数据流。在假设失败的情况下,JVM将去优化,退回至解释执行并重新收集相关profile。


-问题25: Spring创建动态代理有哪些方式,各种有何特点?

-tags: java

-解答:
有两种方式:

前者只有代理实现了接口的类;后者不能对final修饰的类进行代理,也不能处理final修饰的方法


-问题26: ThreadLocal的基本结构是怎样的,什么情况下会出现内存泄漏?

另外,ThreadLocalMap是如何处理hash 冲突问题的?

-tags: java,并发

-解答:
如图,

pic

再看下图:

pic

这么分析似乎是“弱引用”导致了泄漏,其实不是。事实上,如果是强引用,情况只会更严重,弱引用试图减少泄漏的可能,只不过由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

其实,对于keynullEntry,下一次ThreadLocalMap调用setgetremove的时候会被清除。所以真正需要做的是在不需要再用到该ThreadLocal时调用remove清除之。

Hash冲突问题

HashMap 的数据结构是数组+链表,而
ThreadLocalMap的数据结构仅仅是数组。ThreadLocalMap是通过开放地址法来解决hash冲突的问题。

开放地址法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入。

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。

这种做法有明显的缺点:

之所以被 ThreadLocal采用,是因为:
ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上内部Hash算法的设计能实现低冲突概率。


-问题27: wait/notify机制与park/unpark机制有何异同?

我们知道,Object对象的wait和notify方法可以实现线程的阻塞和唤醒。
LockSupport的park和unpark也可以实现,那么,它们有何相同点和不同点呢?

-tags: java,并发

-解答:
先看它们的使用姿势:

// wait/notify
synchronized (obj) {
    while (<condition does not hold>)
        ……
        obj.wait();
    ……
    obj.notifyAll();
}

// LockSupport
LockSupport.unpark(Thread.currentThread());
LockSupport.park(Thread.currentThread());

不同点

从使用上都可以发现第一个不同点:

// 1次unpark给线程1个许可
LockSupport.unpark(Thread.currentThread());
// 如果线程非阻塞重复调用没有任何效果
LockSupport.unpark(Thread.currentThread());
// 消耗许可
LockSupport.park(Thread.currentThread());
// 阻塞
LockSupport.park(Thread.currentThread());

开发可以不用担心park的时序问题,否则,如果park必须要在unpark之前,那么给编程带来很大的麻烦!wait/notify机制则比较麻烦。比如线程B要用notify通知线程A,那么线程B要确保线程A已经在wait调用上等待了,否则线程A可能永远都在等待。

相同点

LockSupport的park和Object的wait一样也能响应中断


-问题28: G1垃圾回收器的MaxGCPauseMillis参数

-tags: java

-解答:

第一问

先解释一个概念,CSet(collection set):在一次垃圾收集过程中被收集的区域集合

第二问

先明确能容忍的最大暂停时间,我们需要在这个限度范围内设置。

注意需要在吞吐量跟MaxGCPauseMillis之间做一个平衡。

如果MaxGCPauseMillis设置的过小,那么GC就会频繁,吞吐量就会下降。如果MaxGCPauseMillis设置的过大,应用程序暂停时间就会变长。


-问题29: ThreadPoolExecutor的使用

-tags: java,并发

-解答:
这是两个相关的问题,根据所选任务队列的类型,ThreadPoolExecutor 会决定何时启动一个新线程。

Direct handoffs

此时ThreadPoolExecutor 搭配的是 SynchronousQueue。如果所有的线程都在忙碌,而且池中的线程数尚未达到最大,则新任务会启动一个新线程。这个队列没办法保存等待的任务:如果来了一个任务,创建的线程数已经达到最大值,而且所有线程都在忙碌,则新的任务总是会被拒绝。所以,建议将最大线程数指定为一个非常大的值

Bounded queues

使用了有界队列(如 ArrayBlockingQueue)的ThreadPoolExecutor 会采用一个非常复杂的算法。比如,假设池的核心大小为 4,最大为 8,所用的 ArrayBlockingQueue 最大为 10。随着任务到达并被放到队列中,线程池中最多会运行 4 个线程(也就是核心大小)。即使队列完全填满,也就是说有 10 个处于等待状态的任务,ThreadPoolExecutor 也是只利用 4 个线程。

如果队列已满,而又有新任务加进来,此时才会启动一个新线程。这里不会因为队列已满而拒绝该任务,相反,会启动一个新线程。新线程会运行队列中的第一个任务,为新来的任务腾出空间

Unbounded queues

如果 ThreadPoolExecutor 搭配的是无界队列(比如 LinkedBlockedingQueue),则不会拒绝任何任务(因为队列大小没有限制)。这种情况下,ThreadPoolExecutor 最多仅会按最小线程数创建线程,也就是说,最大线程池大小被忽略了


-问题30: ForkJoinPool的使用

-tags: java,并发

-解答:
首先,ForkJoinPool实现了 ExecutorService 接口,是一个标准的线程池。独特之处在于,它是为配合分治算法的使用而设计的:任务可以递归地分解为子集。这些子集可以并行处理,然后每个子集的结果被归并到一个结果中。

实现分治算法时,会创建大量的任务,但希望这些任务只有相对较少的几个线程来管理。

一个问题是,所有任务都要等待它们派生出的任务先完成,然后才能完成。
这使得很难使用 ThreadPoolExecutor 高效实现这个算法。ThreadPoolExecutor 内的线程无法将另一个任务添加到队列中并等待其完成,一旦线程进入等待状态,就无法使用该线程执行它的某个子任务了。

ForkJoinPool 则允许其中的线程创建新任务,之后挂起当前的任务。当任务被挂起时,线程可以执行其他等待的任务

看以下代码片段,fork() 和 join() 方法是这里的关键:没有这些方法,实现这类递归会非常痛苦。这些方法使用了一系列内部的、从属于每个线程的队列来操纵任务,并将线程从执行一个任务切换到执行另一个。细节对开发者是透明的。

ForkJoinTask left = new ForkJoinTask(left, mid);
left.fork();
ForkJoinTask right = new ForkJoinTask(mid, right);
right.fork();
Long count = left.join() + right.join();

ForkJoinPool 还有一个额外的特性,它实现了工作窃取(work-stealing)。每个工作线程都有自己所创建任务的队列。线程会优先处理自己队列中的任务,但如果这个队列已空,它会从其他线程的队列中窃取任务(这是个双端队列,从自己的队列中取时遵循LIFO,窃取任务时遵循FIFO)。其结果是,即使 200 万个任务中有一个需要很长的执行时间,ForkJoinPool 中的其他线程也可以分担其余的随便什么任务。ThreadPoolExecutor 则不会这样:如果一个任务需要很长的时间,其他线程并不能处理额外的工作

一般而言,如果任务是均衡的,使用分段的ThreadPoolExecutor性能更好;而如果任务是不均衡的,则使用 ForkJoinPool性能更好。


-问题31: JVM是怎么实现synchronized的

即,用synchronized关键字来对程序进行加锁的原理

-tags: java,并发

-解答:
当声明 synchronized 代码块时,编译而成的字节码将包含monitorentermonitorexit指令。这两种指令均会使用synchronized关键字括号里的引用,作为所要加锁解锁的锁对象。

可以抽象地理解为每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针

当执行 monitorenter 时,如果目标锁对象的计数器为 0,那么说明它没有被其他线程所持有。在这个情况下,Java 虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加 1。

在目标锁对象的计数器不为 0 的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加 1,否则需要等待,直至持有线程释放该锁。

当执行 monitorexit 时,Java 虚拟机则需将锁对象的计数器减 1。当计数器减为 0 时,那便代表该锁已经被释放掉了。

当进行加锁操作时,JVM还会判断锁的类型。

对象头中的标记字段(mark word)的最后两位便被用来表示该对象的锁状态。其中,00 代表轻量级锁,01 代表无锁(或偏向锁),10 代表重量级锁。

重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。

轻量级锁采用CAS操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。

偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。


-问题32: 什么是CAS的ABA问题,如何避免?

-tags: java,并发

-解答:

ABA问题的根本原因在于对象值本身与状态被画上了等号。解决方式就是去除这个等号,不使用值本身,而使用版本戳version做对比。如java中的AtomicStampedReference。其compareAndSet变成:

boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)

-问题33: Lock接口实现的锁和synchronized关键字实现的锁有何不同?

-tags: java,并发

-解答:


-问题34: CountDownLatch和CyclicBarrier的使用有何区别?

-tags: java,并发

-解答:
它们都是阻塞一些行为直至某个事件发生,但Latch是等待某个事件发生,而Barrier是等待线程

闭锁(Latch)就像一个大门,未到达结束状态相当于大门紧闭,不让任何线程通过。
而到达结束状态后,大门敞开,让所有的线程通过,但是一旦敞开后不会再关闭
闭锁可以用来确保一些活动在某个事件发生后执行。

我们可以用栅栏(Barrier)将一个问题分解成多个独立的子问题,并在执行结束后在同一处进行汇集。
当线程到达汇集地后调用await,await方法会阻塞直至其他线程也到达汇集地。
如果所有的线程都到达就可以通过栅栏,也就是所有的线程得到释放,而且栅栏也可以被重新利用。

总之,Latch是听口令行动,Barrier是看人数行动。


-问题35: SynchronousQueue的特性

-tags: java,并发

-解答:
SynchronousQueue与其他BlockingQueue有着不同特性:

显示,这是特殊的生产者-消费者模式。一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品。

SynchronousQueue的一个使用场景是在线程池里。Executors.newCachedThreadPool()就使用了它。目的就是保证“对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务”。

SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue


-问题36: 什么是FutureTask,有什么用?

-tags: java,并发

-解答:
FutureTask除了实现Future接口,还实现了Runnable接口。因此,可以交给Executor执行,也可以由调用的线程直接执行(FutureTask.run())。FutureTask还可以确保即使调用了多次run方法,它都只会执行一次任务

常用的场景是在高并发环境下确保任务只执行一次

举一个例子,假设有一个带key的连接池,当key存在时,即直接返回key对应的对象;当key不存在时,则创建连接。

在高并发的情况下有可能出现Connection被创建多次的现象(想想如何出现?)。创造性的解决思路是,当key不存在时,不是先创建Connection再放到Pool中,而只是把这个“意愿”表达出来,并放到connectionPool之后执行。

    public Connection getConnection(String key) throws Exception {
        FutureTask<Connection> connectionTask = connectionPool.get(key);
        if (connectionTask != null) {
            return connectionTask.get();
        } else {
            Callable<Connection> callable = new Callable<Connection>() {
                @Override
                public Connection call() throws Exception {
                    // 耗时的同步创建连接方法
                    return createConnection();
                }
            };
            FutureTask<Connection> newTask = new FutureTask<Connection>(callable);
            connectionTask = connectionPool.putIfAbsent(key, newTask);
            if (connectionTask == null) {
                connectionTask = newTask;
                connectionTask.run();
            }
            return connectionTask.get();
        }
    }

核心是connectionPool.get不再直接返回Connection,而是返回FutureTask<Connection>
如果为空,不是直接新建连接,而是通过callable表达这个意愿,这是非常廉价快速的操作。哪怕因并发,两个线程都表达了这个意愿也没有关系,因为putIfAbsent保证只有一个意愿会被保存。设置这个意愿的线程会触发run,其余的都在get处阻塞,直到run结束。


-问题37: Volatile关键字的特性

-tags: java,并发

-解答:
volatile修饰的变量具有下列特性:

考虑下面代码:

instance = new Singleton()

这里看起来是一句话,但实际上它并不是一个原子操作。

这句话被编译成8条汇编指令,大致做了3件事情:
  1.给Singleton的实例分配内存
  2.初始化Singleton的构造器
  3.将instance对象指向分配的内存空间(注意到这步instance就非null了)

第二点和第三点的顺序是无法保证的,也就是说,执行顺序可能是1-2-3也可能是1-3-2,如果是后者,并且在3执行完毕、2未执行之前,被切换到线程二上,这时候instance因为已经在线程一内执行过了第三点,instance已经是非空了,所以线程二直接拿走instance,然后使用,然后顺理成章地报错。

用volatile后,保证了instance变量的原子性,禁止把3重排序到前面,即禁止volatile变量赋值之前的重排序。


-问题38: 什么时候需要自定义类加载器?

-tags: java

-解答:


上一篇下一篇

猜你喜欢

热点阅读