21Java网易面经备战版 第二弹

2022年05月14日 阅读数:7
这篇文章主要向大家介绍21Java网易面经备战版 第二弹,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

21Java网易面经备战版 第二弹_面试复习
21Java网易面经备战版 第二弹_java_02
没想到昨天 一个没有写完的 面试备战笔记 上了牛客热搜第一
成就值一下涨了一千多 刷了三年题 也才勉强凑够一千 成就值
那我就 趁热打铁 再准备一篇
昨天那个可能 太难了
这回这个就正经多了。html

文章目录

面经:网易互联网 暑期实习一面面经

网易互联网 暑期实习一面面经 源地址java

3月19投简历,3月22约面,3月24下午14:15面试面试

1.介绍一下MapReduce

这个我没学过 应该也不会问我 先pass 加入代办事项 谁会也能够直接评论回答下redis

2.介绍一下红黑树,红黑树和普通二叉树的区别

就是红黑树 弱平衡 查找快 可是 其余平衡二叉树 插入的效率低 红黑树性能最好
红黑树须要的连续内存空间少 红黑树适合内存
B+树节点都挤在一块儿 须要大量连续内存空间 适合硬盘的操做算法

红黑树

21Java网易面经备战版 第二弹_面试复习_03

红黑树定义和性质
红黑树是一种含有红黑结点并能自平衡的二叉查找树。它必须知足下面性质:数据库

  1. 性质1:每一个节点要么是黑色,要么是红色。编程

  2. 性质2:根节点是黑色。数组

  3. 性质3:每一个叶子节点(NIL)是黑色。缓存

  4. 性质4:每一个红色结点的两个子结点必定都是黑色。安全

  5. 性质5:任意一结点到每一个叶子结点的路径都包含数量相同的黑结点。
    从性质5又能够推出:

  6. 性质5.1:若是一个结点存在黑子结点,那么该结点确定有两个子结点

红黑树并非一个完美平衡二叉查找树,从图1能够看到,根结点P的左子树显然比右子树高,但左子树和右子树的黑结点的层数是相等的,也即任意一个结点到到每一个叶子结点的路径都包含数量相同的黑结点(性质5)。因此咱们叫红黑树这种平衡为黑色完美平衡。

红黑树能自平衡,它靠的是什么?三种操做:左旋、右旋和变色。

  • 左旋:以某个结点做为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图3。
  • 右旋:以某个结点做为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋- 转结点的左子结点,右子结点保持不变。如图4。
    变色:结点的颜色由红变黑或由黑变红。

做者:安卓大叔 连接:https://www.jianshu.com/p/e136ec79235c 来源:简书
著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

为何用红黑树

首先红黑树在主流的平衡树里面属于平衡度比较低的(树高最坏能够到2logn),
相对的平衡操做成本也更低,
工程上通常不会碰到最坏状况,
因此平衡操做成本低的红黑树相对性能更好。
其次红黑树须要的额外存储成本是最低的(1bit表示红/黑就好了,AVL都至少须要2bit),
较小的空间开销也能够带来更高的缓存效率,从而提升性能。
最后,工程上更倾向于使用有成功案例的方案,
红黑树早期的流行使得你们在没有很特殊的需求时都会优先考虑红黑树。
极可能一些场景下别的数据结构性能比红黑树还好,
可是“红黑树也不是不能用,
代码还好抄,就先将就用着吧”。

做者:鱼你太美 连接:https://www.zhihu.com/question/27542473/answer/840995214
来源:知乎 著做权归做者全部。商业转载请联系做者得到受权,非商业转载请注明出处。

引伸 HashMap为何用红黑树而不用B树?

  • B/B+树多用于外存上时,B/B+也被成为一个磁盘友好的数据结构。

  • HashMap原本是数组+链表的形式,链表因为其查找慢的特色,因此须要被查找效率更高的树结构来替换。若是用B/B+树的话,在数据量不是不少的状况下,数据都会“挤在”一个结点里面,这个时候遍历效率就退化成了链表。

3.HashMap 和 TreeMap的区别

区别
定义区别

TreeMap是排序的而HashMap不是

先看HashMap的定义:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

再看TreeMap的定义:

public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable
底层结构

HashMap的底层结构是Node的数组:

transient Node<K,V>[] table

当HashMap中存储的数据过多的时候,table数组就会被装满,这时候就须要扩容,HashMap的扩容是以2的倍数来进行的。而loadFactor就指定了何时须要进行扩容操做。默认的loadFactor是0.75。

public HashMap(int initialCapacity, float loadFactor) 

TreeMap的底层是一个Entry:实现是一个红黑树,方便用来遍历和搜索。

private transient Entry<K,V> root

TreeMap的构造函数能够传入一个Comparator,实现自定义的比较方法。

public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
排序区别

TreeMap输出的结果是排好序的,而HashMap的输出结果是不定的。

Null值的区别
  • HashMap能够容许一个null key和多个null value。
    HashMap会报出: NullPointerException。

  • 而TreeMap不容许null key,可是能够容许多个null value。

性能区别
  • HashMap的底层是Array,因此HashMap在添加,查找,删除等方法上面速度会很是快。而TreeMap的底层是一个Tree结构,因此速度会比较慢。

  • 另外HashMap由于要保存一个Array,因此会形成空间的浪费,而TreeMap只保存要保持的节点,因此占用的空间比较小。

  • HashMap若是出现hash冲突的话,效率会变差,不过在java 8进行TreeNode转换以后,效率有很大的提高。

  • TreeMap在添加和删除节点的时候会进行重排序,会对性能有所影响。

共同点

二者都不容许duplicate key,二者都不是线程安全的。

深刻理解HashMap和TreeMap的区别
https://www.cnblogs.com/flydean/p/hashmap-vs-treemap.html#%E6%8E%92%E5%BA%8F%E5%8C%BA%E5%88%AB

扩展 HashMap和HashTable的区别

  • Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,因此HashMap比Hashtable的性能高一点。
  • Hashtable不容许使用null做为key和value,若是试图把null值放进Hashtable中,将会引起空指针异常,但HashMap可使用null做为key或value。

4.TreeMap的底层如何实现

TreeMap 底层原理

TreeMap基于红黑树(Red-Black tree)实现。
映射根据其键的天然顺序进行排序,
或者根据建立映射时提供的 Comparator 进行排序,
具体取决于使用的构造方法。
TreeMap的基本操做containsKey、get、put、remove方法,
它的时间复杂度是log(N)。

TreeMap包含几个重要的成员变量:
root、size、comparator。
其中root是红黑树的根节点。
它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key、value、left、right、parent和color。
Entry节点根据根据Key排序,包含的内容是value。
Entry中key比较大小是根据比较器comparator来进行判断的。
size是红黑树的节点个数。

《牛客Java面试宝典》 第1章 第6节 Java基础-6
https://www.nowcoder.com/tutorial/10070/489ddf1bff5e419ba8f8f99c6ff6e393

5.介绍一下 ConcurrentHashMap

参考 HashMap 和 ConcurrentHashMap 的区别
  • HashMap是非线程安全的,这意味着不该该在多线程中对这些Map进行修改操做,不然会产生数据不一致的问题,甚至还会由于并发插入元素而致使链表成环,这样在查找时就会发生死循环,影响到整个应用程序。

  • Collections工具类能够将一个Map转换成线程安全的实现,其实也就是经过一个包装类,而后把全部功能都委托给传入的Map,而包装类是基于synchronized关键字来保证线程安全的(Hashtable也是基于synchronized关键字),底层使用的是互斥锁,性能与吞吐量比较低。

  • ConcurrentHashMap的实现细节远没有这么简单,所以性能也要高上许多。它没有使用一个全局锁来锁住本身,而是采用了减小锁粒度的方法,尽可能减小由于竞争锁而致使的阻塞与冲突,并且ConcurrentHashMap的检索操做是不须要锁的。

补充 介绍一下ConcurrentHashMap是怎么实现的?JDK 1.7中的实现:
  • 在 jdk 1.7 中,
    ConcurrentHashMap 是由 Segment 数据结构和 HashEntry 数组结构构成,采起分段锁来保证安全性。Segment 是 ReentrantLock 重入锁,在 ConcurrentHashMap 中扮演锁的角色,HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组,一个 Segment 里包含一个 HashEntry 数组,Segment 的结构和 HashMap 相似,是一个数组和链表结构。

21Java网易面经备战版 第二弹_java_04

  • JDK 1.8中的实现:
    JDK1.8 的实现已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 Synchronized 和 CAS 来操做,整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,可是已经简化了属性,只是为了兼容旧版本。
    21Java网易面经备战版 第二弹_面试复习_05
补充JDK1.7 的 ConcurrentHashMap是怎么分段分组的?
  • get操做:
    Segment的get操做实现很是简单和高效,先通过一次再散列,而后使用这个散列值经过散列运算定位到 Segment,再经过散列算法定位到元素。get操做的高效之处在于整个get过程都不须要加锁,除非读到空的值才会加锁重读。缘由就是将使用的共享变量定义成 volatile 类型。

  • put操做:
    当执行put操做时,会经历两个步骤:

  1. 判断是否须要扩容;
  2. 定位到添加元素的位置,将其放入 HashEntry 数组中。
    插入过程会进行第一次 key 的 hash 来定位 Segment 的位置,若是该 Segment 尚未初始化,即经过 CAS 操做进行赋值,而后进行第二次 hash 操做,找到相应的 HashEntry 的位置,这里会利用继承过来的锁的特性,在将数据插入指定的 HashEntry 位置时(尾插法),会经过继承 ReentrantLock 的 tryLock() 方法尝试去获取锁,若是获取成功就直接插入相应的位置,若是已经有线程获取该Segment的锁,那当前线程会以自旋的方式去继续的调用 tryLock() 方法去获取锁,超过指定次数就挂起,等待唤醒。

6.++i 是原子操做吗

不是

7.如何保证++i的原子操做

  1. cas
  2. 加锁
  3. synchronized
  4. 原子变量
    在java的util.concurrent.atomic包中提供了建立了原子类型变量的工具类,使用该类能够简化线程同步。例如AtomicInteger 表能够用原子方式更新int的值,可用在应用程序中(如以原子方式增长的计数器),但不能用于替换Integer。可扩展Number,容许那些处理机遇数字类的工具和实用工具进行统一访问。

8.Volatile能够保证++i原子操做吗

volatile关键字为域变量的访问提供了一种免锁机制,
使用volatile修饰域至关于告诉虚拟机该域可能会被其余线程更新,
所以每次使用该域就要从新计算,
而不是使用寄存器中的值。
须要注意的是,volatile不会提供任何原子操做,它也不能用来修饰final类型的变量。

9.Volatile的底层如何实现

  • volatile能够保证线程可见性且提供了必定的有序性,可是没法保证原子性。
  • 在JVM底层volatile是采用“内存屏障”来实现的。
  • 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,
  • 加入volatile关键字时,会多出一个lock前缀指令,
  • lock前缀指令实际上至关于一个内存屏障,内存屏障会提供3个功能:
  1. 它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;
  2. 它会强制将对缓存的修改操做当即写入主存;
  3. 若是是写操做,它会致使其余CPU中对应的缓存行无效。

10.给100w条数据,插入ArrayList里,时间复杂度是多少

  • 在ArrayList中,底层数组存/取元素效率很是的高(get/set),时间复杂度是O(1),而查找,插入和删除元素效率彷佛不过高,时间复杂度为O(n)。

  • 当咱们ArrayLIst里有大量数据时,这时候去频繁插入/删除元素会触发底层数组频繁拷贝,效率不高,还会形成内存空间的浪费,

也就是 扩容了100W 就能够 复杂度为1 不扩容 就是一点一点 扩容 复杂度 nlogn

11.Synchronized 的底层是如何实现的

  1. synchronized做用在代码块时,它的底层是经过monitorenter、monitorexit指令来实现的。
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

21Java网易面经备战版 第二弹_面试复习_06

  • monitorenter:
    每一个对象都是一个监视器锁(monitor),
    当monitor被占用时就会处于锁定状态,
    线程执行monitorenter指令时尝试获取monitor的全部权,
    过程以下:
    • 若是monitor的进入数为0,
    • 则该线程进入monitor,
    • 而后将进入数设置为1,
    • 该线程即为monitor的全部者。
    • 若是线程已经占有该monitor,
    • 只是从新进入,
    • 则进入monitor的进入数加1。
    • 若是其余线程已经占用了monitor,
    • 则该线程进入阻塞状态,
    • 直到monitor的进入数为0,
    • 再从新尝试获取monitor的全部权。
  • monitorexit:
    • 执行monitorexit的线程必须是objectref所对应的monitor持有者。
    • 指令执行时,
    • monitor的进入数减1,
    • 若是减1后进入数为0,
    • 那线程退出monitor,
    • 再也不是这个monitor的全部者。
    • 其余被这个monitor阻塞的线程能够尝试去获取这个monitor的全部权。

monitorexit指令出现了两次,
第1次为同步正常退出释放锁,
第2次为发生异步退出释放锁。

  1. 同步方法
public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

21Java网易面经备战版 第二弹_面试复习_07

  • 从反编译的结果来看,方法的同步并无经过 monitorenter 和 monitorexit 指令来完成,

  • 不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。

  • JVM就是根据该标示符来实现方法的同步的:

  • 当方法调用时,

    • 调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,
    • 若是设置了,执行线程将先获取monitor,
    • 获取成功以后才能执行方法体,
    • 方法执行完后再释放monitor。
    • 在方法执行期间,
    • 其余任何线程都没法再得到同一个monitor对象。
  1. 总结

两种同步方式本质上没有区别,
只是方法的同步是一种隐式的方式来实现,
无需经过字节码来完成。
两个指令的执行是JVM经过调用操做系统的互斥原语mutex来实现,
被阻塞的线程会被挂起、等待从新调度,
会致使“用户态和内核态”两个态之间来回切换,对性能有较大影响。

补充 说一说synchronized与Lock的区别
  • synchronized是Java关键字,在JVM层面实现加锁和解锁;Lock是一个接口,在代码层面实现加锁和解锁。
  • synchronized能够用在代码块上、方法上;Lock只能写在代码里。
  • synchronized在代码执行完或出现异常时自动释放锁;Lock不会自动释放锁,须要在finally中显示释放锁。
  • synchronized会致使线程拿不到锁一直等待;Lock能够设置获取锁失败的超时时间。
  • synchronized没法得知是否获取锁成功;Lock则能够经过tryLock得知加锁是否成功。
  • synchronized锁可重入、不可中断、非公平;Lock锁可重入、可中断、可公平/不公平,并能够细分读写锁以提升效率。

12.b+树在什么状况下高度会变高

一个高度为 3 的 B+ 树大概能够存放 1170 × 1170 × 16 = 21902400 行数据,已是千万级别的数据量了。
leaf page 和 index page都变满 树的高度会增长
B+树插入新元素时,可能会遇到3种状况

21Java网易面经备战版 第二弹_java_08

原文连接:https://blog.csdn.net/wei_wenbo/article/details/50819651

补充 为何 MySQL 的索引要使用 B+ 树而不是其它树形结构?好比 B 树?

简单版本回答是:

由于 B 树无论叶子节点仍是非叶子节点,都会保存数据,这样致使在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的状况下要保存大量数据,只能增长树的高度,致使 IO 操做变多,查询性能变低。

https://zhuanlan.zhihu.com/p/86137284

13.项目中的访问量最大能够达到多少

牛客Java集训营 秒杀项目
https://www.nowcoder.com/courses/cover/live/537
21Java网易面经备战版 第二弹_java_09

14.为何用redis,用HashMap不是也能够吗,并且更简单

15.Redis如何与MySQL进行数据同步

  • 四种同步策略:
    想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:
  1. 先更新缓存,再更新数据库;
  2. 先更新数据库,再更新缓存;
  3. 先删除缓存,再更新数据库;
  4. 先更新数据库,再删除缓存。
    从这4种同步策略中,咱们须要做出比较的是:

更新缓存与删除缓存哪一种方式更合适?
应该先操做数据库仍是先操做缓存?

下面,咱们来分析一下,应该采用更新缓存仍是删除缓存的方式。

  1. 更新缓存
  • 优势:每次数据变化都及时更新缓存,因此查询时不容易出现未命中的状况。
  • 缺点:更新缓存的消耗比较大。若是数据须要通过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。若是是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
  1. 删除缓存
  • 优势:操做简单,不管更新操做是否复杂,都是将缓存中的数据直接删除。
  • 缺点:删除缓存后,下一次查询缓存会出现未命中,这时须要从新读取一次数据库。

从上面的比较来看,通常状况下,删除缓存是更优的方案。
先操做数据库仍是缓存:

下面,咱们再来分析一下,应该先操做数据库仍是先操做缓存。

  1. 首先,咱们将先删除缓存与先更新数据库,在出现失败时进行一个对比:
    21Java网易面经备战版 第二弹_面试复习_10
    如上图,是先删除缓存再更新数据库,在出现失败时可能出现的问题:
  2. 进程A删除缓存成功;
  3. 进程A更新数据库失败;
  4. 进程B从缓存中读取数据;
  5. 因为缓存被删,进程B没法从缓存中获得数据,进而从数据库读取数据;
  6. 进程B从数据库成功获取数据,而后将数据更新到了缓存。

最终,缓存和数据库的数据是一致的,但仍然是旧的数据。而咱们的指望是两者数据一致,而且是新的数据。
21Java网易面经备战版 第二弹_java_11
如上图,是先更新数据库再删除缓存,在出现失败时可能出现的问题:

  1. 进程A更新数据库成功;
  2. 进程A删除缓存失败;
  3. 进程B读取缓存成功,因为缓存删除失败,因此进程B读取到的是旧的数据。

最终,缓存和数据库的数据是不一致的。
通过上面的比较,咱们发如今出现失败的时候,
是没法明确分辨出先删缓存和先更新数据库哪一个方式更好,由于它们都存在问题。后面咱们会进一步对这两种方式进行比较,可是在这里咱们先探讨一下,上述场景出现的问题,应该如何解决呢?

实际上,不管上面咱们采用哪一种方式去同步缓存与数据库,
在第二步出现失败的时候,
都建议采用重试机制解决,
由于最终咱们是要解决掉这个错误的。
而为了不重试机制影响主要业务的执行,
通常建议重试机制采用异步的方式执行,以下图:

21Java网易面经备战版 第二弹_面试复习_12
这里咱们按照先更新数据库,再删除缓存的方式,来讲明重试机制的主要步骤:

  1. 更新数据库成功;
  2. 删除缓存失败;
  3. 将此数据加入消息队列;
  4. 业务代码消费这条消息;
  5. 业务代码根据这条消息的内容,发起重试机制,即从缓存中删除这条记录。
    好了,下面咱们再将先删缓存与先更新数据库,在没有出现失败时进行对比:
    21Java网易面经备战版 第二弹_面试复习_13

如上图,是先删除缓存再更新数据库,在没有出现失败时可能出现的问题:

  1. 进程A删除缓存成功;
  2. 进程B读取缓存失败;
  3. 进程B读取数据库成功,获得旧的数据;
  4. 进程B将旧的数据成功地更新到了缓存;
  5. 进程A将新的数据成功地更新到数据库。

可见,进程A的两步操做均成功,
但因为存在并发,
在这两步之间,
进程B访问了缓存。
最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,两者数据不一致。

21Java网易面经备战版 第二弹_面试复习_14
如上图,是先更新数据库再删除缓存,再没有出现失败时可能出现的问题:

  1. 进程A更新数据库成功;
  2. 进程B读取缓存成功;
  3. 进程A更新数据库成功。

可见,最终缓存与数据库的数据是一致的,而且都是最新的数据。
但进程B在这个过程里读到了旧的数据,
可能还有其余进程也像进程B同样,在这两步之间读到了缓存中旧的数据,
但由于这两步的执行速度会比较快,因此影响不大。
对于这两步以后,其余进程再读取缓存数据的时候,就不会出现相似于进程B的问题了。

最终结论:

通过对比你会发现,
先更新数据库、再删除缓存是影响更小的方案。
若是第二步出现失败的状况,
则能够采用重试机制解决问题。

扩展阅读 延时双删

上面咱们提到,
若是是先删缓存、再更新数据库,
在没有出现失败时可能会致使数据的不一致。
若是在实际的应用中,
出于某些考虑咱们须要选择这种方式,
那有办法解决这个问题吗?

答案是有的,那就是采用延时双删的策略,

延时双删的基本思路以下:

  1. 删除缓存;
  2. 更新数据库;
  3. sleep N毫秒;
  4. 再次删除缓存。
    阻塞一段时间以后,再次删除缓存,
    就能够把这个过程当中缓存中不一致的数据删除掉。
    而具体的时间,要评估你这项业务的大体时间,按照这个时间来设定便可。

采用读写分离的架构怎么办?

若是数据库采用的是读写分离的架构,那么又会出现新的问题,以下图:
21Java网易面经备战版 第二弹_java_15

进程A先删除缓存,再更新主数据库,而后主库将数据同步到从库。
而在主从数据库同步以前,可能会有进程B访问了缓存,
发现数据不存在,进而它去访问从库获取到旧的数据,而后同步到缓存。
这样,最终也会致使缓存与数据库的数据不一致。
这个问题的解决方案,依然是采用延时双删的策略,
可是在评估延长时间的时候,要考虑到主从数据库同步的时间。

第二次删除失败了怎么办?

若是第二次删除依然失败,则能够增长重试的次数,
可是这个次数要有限制,
当超出必定的次数时,要采起报错、记日志、发邮件提醒等措施。

16.一道和Java并发编程相关的题目

估计是 三个线程
同时执行
顺序执行
交替执行
之类的题目

顺序执行

使用 join

public class ThreadTest1 {
// T一、T二、T3三个线程顺序执行
public static void main(String[] args) {
    Thread t1 = new Thread(new Work(null));
    Thread t2 = new Thread(new Work(t1));
    Thread t3 = new Thread(new Work(t2));
    t1.start();
    t2.start();
    t3.start();
 
}
static class Work implements Runnable {
    private Thread beforeThread;
    public Work(Thread beforeThread) {
        this.beforeThread = beforeThread;
    }
    public void run() {
        if (beforeThread != null) {
            try {
                beforeThread.join();
                System.out.println("thread start:" + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else {
            System.out.println("thread start:" + Thread.currentThread().getName());
        }
    }
 }
}
同时执行 CountDownLatch
package thread;

import java.util.concurrent.CountDownLatch;

public class TestCountDownLatch {

    class Worker implements Runnable{

        CountDownLatch countDownLatch;

        Worker(CountDownLatch countDownLatch){
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                countDownLatch.await(); // 等待其它线程
                System.out.println(Thread.currentThread().getName() + "启动@" + System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void doTest() throws InterruptedException {
        final int N = 5; // 线程数
        CountDownLatch countDownLatch = new CountDownLatch(N);
        for(int i=0;i<N;i++){
            new Thread(new Worker(countDownLatch)).start();
            countDownLatch.countDown();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestCountDownLatch testCountDownLatch = new TestCountDownLatch();
        testCountDownLatch.doTest();
    }
}

交替执行-使用-ReentrantLock
public class AlterThreadTest {
    private ReentrantLock lock = new ReentrantLock();
    Condition aCondition = lock.newCondition();
    Condition bCondition = lock.newCondition();

    public static void main(String[] args) {
        AlterThreadTest test = new AlterThreadTest();

        test.new AOutput().start();
        test.new BOutput().start();
    }

    class AOutput extends Thread {

        @Override
        public void run() {
            while (true) {
                try {
                    lock.lock();
                    System.out.println("A");

                    bCondition.signal();
                    aCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    class BOutput extends Thread {

        @Override
        public void run() {
            while (true) {
                try {
                    lock.lock();
                    System.out.println("B");
                    aCondition.signal();
                    bCondition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

17.说一说你最近看过的书籍

剑指offer
jvm虚拟机
架构设计
MySQL必知必会

18.说一说你遇到过的有挑战事情,如何去解决

服务器被黑了 防患于未然

题目来源

做者:yhs98
连接:https://www.nowcoder.com/discuss/622474?type=all&order=time&pos=&page=1&channel=-1&source_id=search_all_nctrack
来源:牛客网