当前位置: 首页 > 工具软件 > 蚂蚁放置2 > 使用案例 >

2020 03 15 蚂蚁金服实习电话一面

酆光熙
2023-12-01


蚂蚁金服商家中台事业部

笔试

  • 生产者-消费者模型,线程A产生整数 n,线程B输出 n 的阶乘。
  • 给一个有序数组 和 数字 n,找到数字 n 的下标。

面试

1.Java基础部分

  • String是不可变的,它为什么不可变?
  • StringBuilder 和 StringBuffer 是怎么实现可变的?
  • ArrayList 和 LinkedList 的区别,和使用场景?
  • equals需要满足什么条件?
  • HashMap 为什么要同时实现 equals 和 hashCode 两个方法?
  • HashMap 发生哈希冲突是怎么处理的?
  • Java 的 IO 模型? NIO?

2.OOP部分

  • 抽象类和接口的区别?使用场景举个例子吧。
           为什么你说抽象类不适合维护和更新?
    说到了1.8的 default 方法,问:这个 default 方法有什么好处?
  • 继承和组合有什么区别?使用场景?

3.线程部分

  • ThreadLocal 的作用是什么?使用场景?
  • 线程安全?有哪些实现方式?
  • 了解的锁?锁分几种?说一下它们之间的区别?最好说使用场景。
    举个例子,子方法要有锁,多次调用子方法,这样子方法用什么锁比较合适?
  • 并发过程中常用到的队列?刚刚的笔试看你没有用队列,你是怎样考虑的?
  • 并发中除了队列外还有很多集合类,常见的 Map、List 、Set里面都有哪些并发集合类?
  • CopyOnWriteArrayList 是怎么实现的?

4.虚拟机

  • 内存模型讲一下
  • GC讲一下

5.Linux

  • 你常用到的 Linux 命令有哪些?
  • CPU Load过高使用率过高怎么排查?
  • inode是什么?
  • host 文件是做什么的?
  • 磁盘内存不够了,怎么找到大的文件?

6.数据库

  • 索引的概念及其使用情景
  • 事务的概念
  • 隔离级别
  • 乐观锁和悲观锁的区别?使用场景?
  • 简述 MVCC
  • 查询语句、删除语句怎么写可以使乐观锁失效?

7.算法

  • 常见排序算法、复杂度、使用场景?
  • 怎么理解时间复杂度?空间复杂度?
  • 怎么实现双向链表?好处?怎么插入和删除元素?

8.网络

  • TCP连接的过程,为什么要三次握手四次挥手?
  • 短连接和长连接?哪些是短连接,哪些是长连接?
        

9.其他

  • 了解深度学习吗?
  • 怎么学习的?主要通过什么渠道学习?
  • 给我推荐几本书?
  • 在学校通过什么渠道驱动写代码?有没有跟着老师做项目?
  • 做项目遇到的有意思的事情?
  • 在学校有和别人合作的经历吗?有不同想法时怎么变?
  • 你写博客积累到什么程度?
  • 参加的竞赛的背景、遇到的困难?
  • 你有什么想问的?
  • 包括社招,看重基础、思维、编码习惯、技术热情,对Spring有封装… …

复盘

Java基础

String 、StringBuilder、StringBuffer

    String 的底层是个 final 修饰的 char[ ],是常量,因此初始化后就不会改变了;而 StringBuffer 、 StringBuilder 初始默认 是来自父类的长度为 16 的 char[ ]。
    StringBuffer 几乎每个方法都用了 synchronized 修饰 ,
线程安全。
    用 “ + ”联结起两个字符串:
(1)如果是字符串常量,编译时就把它们拼接成了新的 字符串常量。
(2)如果涉及到非常量,创建 StringBuilder 对象,调用 apped() 方法。
     JDK1.5之前是 StringBuffer,1.5之后是 StringBuilder。

ArrayList 和 LinkedList 使用场景?

(1)如果对数据有较多随机访问,ArrayList 效率更高;
( 2 ) 如果更多 插入 或 删除操作,较少的数据读取,LinkedList 更优,不过ArrayList的插入 或 删除操作也不一定比 LinkedList 慢,如果在List靠近末尾的地方插入,那么 ArrayList 只需要移动较少的数据,而 LinkedList 则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

完美的 equals() 方法

(1)首先比较是否引用同一个对象,如果是,直接返回 true 。
(2)比较是否属于同一个类 或者 有无直接、间接的子类继承关系,如果不是、没有,返回 false。如果有继承关系,需要强转成一致的类。
(3)对需要比较的域作比较,基本类型域用“==”,对象域用 equals() 。

讲一下 HashMap 的put() 过程

    如果是数组为空,需要先进行 resize 扩容,默认初始容量是 16 。
    对 key 进行 hashcode 计算,对数组长度取余,求得桶索引,如果索引处的 Node 为 null,就直接插入;否则,遍历链表,判断 key值是否相同,如果相同,新 value 覆盖 旧 value,返回 原 value,如果不相同,说明出现了哈希冲突,需要尾插,这里的 “相同”,就是通过 equlas 进行比较的。

IO
NIO/同步、异步、阻塞、非阻塞

(1)BIO、NIO、AIO 总结
    Java 中的 BIO、NIO和 AIO 理解为是 Java 对操作系统的各种 IO 模型的封装。先来回顾一下这样几个概念:同步与异步,阻塞与非阻塞。

同步: 同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
异步: 异步就是发起一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果。
    同步和异步的区别最大在于 异步调用者不需要等待处理结果,被调用者会通过回调等机制来通知调用者其返回结果。
    
阻塞: 阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
非阻塞: 非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他事情。

    
BIO (Blocking I/O) 同步阻塞
    服务端创建一个 ServerSocket , 然后就是客户端用一个Socket 去连接服务端的那个 ServerSocket, ServerSocket 接收到了一个的连接请求就创建一个Socket和一个线程去跟那个 Socket 进行通讯。接着客户端和服务端就进行阻塞式的通信,客户端发送一个请求,服务端 Socket 进行处理后返回响应,在响应返回前,客户端那边就阻塞等待,什么事情也做不了。
    这种方式的缺点, 每次一个客户端接入,都需要在服务端创建一个线程来服务这个客户端,这样大量客户端来的时候,就会造成服务端的线程数量可能达到了几千甚至几万,这样就可能会造成服务端过载过高,最后崩溃死掉。
    
NIO(Non-blocking IO) 同步非阻塞
    NIO它支持面向缓冲的,基于通道
    ==通道Channel,==用于接收及存储不同的连接与状态 key。
    有 Selector选择器,负责轮询查看不同通道内的请求,做出相应的选择处理。
    
Java中对于NIO的实现:
    首先程序会向选择器 Selector 中注册通道 Channel,和通道所关注的事件,选择器轮询 Channel ,如果其中有某个事件状态符合所注册的通道事件,那么 Selector 就会将它作为 key 集返回给程序,与此同时,类似于读和写这样的事件,就已经将内容存储到了 buffer 中,程序通过 key 感知到对应事件后,可以直接通过 buffer 去做相应的操作。

(比方说,Tomcat 从 6 开始支持 NIO 模型,客户端发送的连接请求都会注册到 多路复用器上,多路复用器轮询到 连接 有 I/O 请求时才启动一个线程进行处理。)

AIO
    异步非阻塞。
     每个连接发送过来的请求,都会绑定一个Buffer,然后通知操作系统去完成异步的读,这个时间你就可以去做其他的事情,等到操作系统完成读之后,就会调用接口,返回操作系统异步读完的数据。这个时候就可以拿到数据进行处理,将数据往回写,在往回写的过程,同样是给操作系统一个 Buffer ,让操作系统去完成写,写完了来通知你。
这里面的主要的区别在于将数据写入的缓冲区后,就不去管它,剩下的去交给操作系统去完成。操作系统写回数据也是一样,写到Buffer里面,写完后通知客户端来进行读取数据。
    
    以上是 Java 对操作系统的各种 IO 模型的封装,【文件的输入、输出】在文件处理时,其实依赖操作系统层面的 IO 操作实现的。【把磁盘的数据读到内存种】操作系统中的 IO 有 5 种:
阻塞、
非阻塞、【轮询】
异步、
IO复用、【多个进程的 IO 注册到管道上】
信号驱动 IO

虚拟机

Hosts 文件

    纯文本形式。
    这个文件包含 IP地址 到 HOST name(主机名)的映射关系,提高解析效率。

CPU Load过高怎么排查?该如何快速排查原因?

    Load:Linux系统中,进程有 3 种状态:阻塞、可运行的【在运行队列 run queue 种】、正在运行的,Load 是指正在运行 和 准备好运行的进程总数。
    CPU Load 过高可能是代码中有 Bug(如 死循环)或者 Full GC次数太多。
    首先要找到哪几个线程在占用 CPU ,之后再通过线程的 id值在堆栈文件中查找具体的线程,看看出来什么问题。
(1)先查看进程号 top
    通过top命令动态查看进程变化,默认 5 秒一更新,会显示PID 、%CPU 等,执行 shift+p 可以以 CPU 的使用排序显示。比如使用CPU 最多的进程是 xxx。
(2)确定进程后 top -Hp xxx 显示 指定进程的线程
    这时显示的 PID 是线程ID。
(3)将线程 ID 从十进制转化成 十六进制
    可以用 Windows 计算器,也可以用 printf %x 十进制数 将输出十六进制形式。
(4)显示堆栈信息

jstack 进程号 |grep 线程号

    显示栈信息,可以关注一下有无 死锁 或者 等待资源、等待获取监视器、阻塞等。
grep: 全局搜索正则表达式,就像在 IDEA 里 ctrl+F 。)

线程安全有哪些实现方式

(1)synchronized 同步方法 、同步块
(2)volatile 保证内存可见性
(3)JUC 包中的 ReentrantLock 可重入锁
(4)线程本地变量,如 ThreadLocal
(5)并发队列
(6)原子操作类

线程本地变量

    ThreadLocal 是个工具类,在 Thread 里有变量 ThreadLocalMap 类型的 threadlocals,为每个线程对象维护一个 线程本地变量,ThreadLocalMap 是 ThreadLocal 的静态内部类,以 ThreadLocal 为 key,以 线程本地变量 为 vaule,当调用 ThreadLocal 的 set 方法时,会先获取当前线程,检查有没有为 这个线程对象 绑定 本地变量,如果没有的话,会创建 map;否则覆盖value。 要注意 :如果当前线程不消亡,这些本地变量就会一直存在,可能会造成内存溢出,所以使用完毕要记得调用 ThreadLocal 的 remove方法,它会调用 Entry 的clear() 方法,提醒 GC,Entry 是弱引用类型。
    还可以讲个类似思想的,ThreadLocalRandom 随机数生成器,它继承 Random ,随机数生成的思想是由 种子 seed 生成新种子,再由新种子计算随机数, Random 类为保证线程安全,使用 CAS操作,每次只能有一个线程可以更新老的种子为新的,失败的线程会自旋 ,直到获取到 种子 作为 自己的当前种子,又去算新的种子 ,从而保证各线程随机数的随机性。而自旋降低了性能。所以 ,ThreadLocalRandom 也像 ThreadLocal 一样,是个工具类,在 Thread 类里有 ==long 类型的变量 ThreadLocalRandomSeed,这样每个线程对象都会持有一个本地种子变量,需要使用随机数的时候才会被初始化,计算随机数时是和 系统的当前时间有关系的,从而保证各线程随机数的随机性。

Reetrant可重入锁

    ReentrantLock 是使用 AQS 实现的可重入独占锁,AQS 的状态state 是 0 表示当前 锁空闲,大于等于 1 表示该锁已经被占用。该锁内部有 公平锁 和 非公平锁,默认是非公平锁。
    线程调用 lock 方法,表示希望获取锁,如果锁没有被其他线程占用,且当前线程之前没有获取过该锁,则当前线程会获取到锁,将锁的拥有者 设置为 当前线程,并把 AQS 状态值设置为 1,然后直接返回,如果该锁已经被其他线程持有,线程会被放入 AQS 队列后阻塞挂起。
    不适合 写少读多的情况。
    

原子操作类

JUC包中提供了一系列原子性操作类,都是用 CAS 实现的,比如 AtomicLong、AtomicInteger.等。
(1) AtomicLong
    可以指定初值,使用 递增、递减方法,或者是 CompareAndSet,它是维护一个 volatile 修饰的 long 变量,递增、递减时候,使用 CAS 修改变量的值,如果设置失败则 自旋 直到设置成功。
(2)JDK8新增了 LongAdder
    从 0 开始,不能指定初值,可以用 add() 实现加法,传入正数、负数皆可,或 decrement() 递减,reset() 置零。
    它在内部维护了一个 volatile 修饰的基值 base 和 一个Cell 【内部类,有 volatile 修饰的 value 】类型的数组 cells ,无线程竞争时,也就是 对 base 使用 CAS 成功了,累加 base 值即可;而如果存在线程竞争,就先会初始化 cells 数组,长度为2,当前线程具体访问 cells 数组里的哪一个 cell ,是由 threadLocalRandomProbe 和 数组长度-1 相与 之后求得的,如果再遇到多个线程访问同一个元素才会以 2 倍扩容,重新计算位置,所以数组长度一直是 2 的幂次方,并且不会大于 CPU 个数,这样可以避免伪共享问题。这样,线程在 某个 cell 上 CAS 失败后,不会自旋,而是去另一个 cells 上尝试修改。最后 ,LongAdder 的真实值是 base 和所有 cell 元素的值的累加。 要注意的是:累加的方法 sum() 并没有对 cells 数组加锁,所以如果其他线程有对 cells 修改值,或者扩容,求出的结果就是不准确的。

(1)synchronized 同步锁

    进入同步块后,尝试获取 JVM 中给 类 和 对象 关联的 监视器锁  ,保证同步块的原子性;将工作内存清空,读共享变量时直接去 主内存 读,写时直接写入主内存,来保证内存可见性。

(2)LockSupport 阻塞锁

    LockSupport 工具类 与 每个使用它的线程 关联一个许可证,默认情况下,调用线程 是不持有许可证的。
    调用 LockSupport. park() 方法的线程如果没有拿到 许可证,就会被阻塞挂起,否则正常返回。
    调用 LockSupport. unpark(thread) 方法,会使参数线程thread 持有许可证。如果 thread 之前因为调用 park() 被挂起,调用 unpark(thread) 后就会被唤醒。


同步器、锁底层的实现:AQS

    AQS: 抽象同步队列,是一个 双向队列,队列元素类型是 Node,线程同步是通过 volatile 修饰的 int 类型的 state 控制的,根据 state 是否属于一个线程,操作 state 的方式分为 独占 EXECLUSIVE 和 共享 SHARE。
    独占:资源是与具体线程绑定的,如果线程获取 acquire() 资源,就使用 tryAcquire() 尝试获取资源,具体是设置 state 的值,成功则直接返回,失败则会被封装成 EXCLUSIVE 类型的 Node 节点尾插到 阻塞队列 ,并调用 LockSupport.park() 方法挂起自己;
    共享:资源 和 具体线程不相关,一个线程获取到资源后,其他线程再来获取时,如果资源可以满足它的需要,就用 CAS 方式获取,如果不能满足需要,会被封装成 SHARE 类型的 Node 节点放入阻塞队列。

    当线程释放release()资源,先尝试用 tryRealse() 修改 state 的值,然后调用 LockSupport.unpark( thread )方法激活 AQS 队列里被阻塞的线程。

    tryAquire 和 tryRealse 都是由具体子类覆写的。【体现 模板设计模式】是根据具体情景使用 CAS 修改 state 的值。

    AQS 中有内部类 ConditionObject 条件变量,JUC包里的 Lock 接口的实现类,比方说 ReentrantLock,使用 newCondition() 实际上就是创建了一个 ConditionObject 对象, 一个 Lock对象可以创建多个 条件变量,每个条件变量都对应一个 条件队列。【单向】
    ConditionObject + lock 锁 类似于 Object + 监视器锁。前提都是要获取到锁,(也就是先要调用锁的 lock() 方法)调用 conditionObject 的 await() 方法,就会构造一个CONDITION 类型的 Node 节点,尾插到 条件队列,然后线程会释放锁,(也就是修改 state 的值)并被阻塞挂起。 而调用 signal 方法,会把条件队列 队头 的线程节点(也就是 等待时间最长的线程 ) 出队,激活线程。)
总结:
    一个锁对应一个 AQS 阻塞队列,对应多个条件变量,每个条件变量都有自己的一个条件队列。 当多个线程同时调用 Lock.lock 方法获取锁时,只有一个线程获取到锁,其他线程都会转化成 Node 节点插入到 lock 锁 对应的 AQS 阻塞队列中,并自旋 CAS 尝试获取锁,如果获取到锁的线程又尝试调用 对应变量的 await() 方法,(和 Object 的 wait() 方法一样,前提都是要获取锁。)则该线程会释放获取到的锁,并转化成 Node 节点插入到 条件变量对应的 条件队列中。这个线程释放了锁,那阻塞队列中就会有线程获取到锁。

    说人话就是:如果有多线程尝试获取锁,只有一个线程获取到锁 ,其他线程就会转化成 Node 尾插到 AQS 阻塞队列, 对于获取到锁的线程,锁 可以对应多个条件变量,而每个条件变量对应一个条件队列,如果条件变量调用了 await() 方法,就释放锁,把这个线程放到 条件队列中,阻塞挂起;如果调用了 signal() 方法,就会从 条件队列 把 队头 移动到 AQS 阻塞队列中去。(因为条件队列中的节点 之前肯定是调用过 await() 方法,是释放掉锁的了,自然就放到 阻塞队列中)。

关于 AQS 的几个问题:

  • 为什么 AQS 要设计成双向队列:
        我的理解是: AQS 的 release() 释放资源时,需要调用 SupportLock.unpark(thread),具体释放的线程,是根据节点的 waitstates 决定的,SIGNAL 状态,也就是值为 -1 ,表示节点释放后,会唤醒下一个节点,所以需要前驱,当前节点的前一个节点是 SIGNAL 状态,当前节点才可以被唤醒。

  • 为什么在 AQS 的 doReleaseShared() 方法中:

    从 head 开始遍历,如果 是 SINGAL,会用 CAS 将状态值修改为 0 ,成功的话就会进入 unparkSuccessor()方法,而 unparkSuccessor() 方法中,如果后继节点是 CANCELL 1:因为超时或中断被放到队列中/线程被取消了,会从 tail 向前遍历。

    这是因为: node.prev = pred 早早执行的,而可能还未执行.next 绑定后继节点操作,如果出现唤醒操作,从头部开始遍历就可能因为后继节点还未绑定,无法通过 .next 获取到下一个节点信息,也就找不到真正需要 unpark 的节点),而从尾部开始扫描则不会导致该问题。


(3)ReentrantLock lock同步锁

     ReentrantLock 可重入独占锁,自身并未继承AQS,而是采用内部类 Sync 继承。AQS 的状态state 是 0 表示当前 锁空闲,大于等于 1 表示该锁已经被占用。该锁内部有 公平锁 和 非公平锁,默认是非公平锁。
    线程调用 lock() 方法,表示希望获取锁,如果锁没有被其他线程占用,且当前线程之前没有获取过该锁,则当前线程会获取到锁,将锁的拥有者 设置为 当前线程,并把 AQS 状态值设置为 1,然后直接返回,如果该锁已经被其他线程持有,线程会被放入 AQS 队列后阻塞挂起。
    线程使用 unlock() 方法释放锁,就会调用 tryRelease() ,通过 CAS 设置状态值 -1,如果减去 1 后状态值为 0,当前线程就会释放锁,否则就只是 减 1 而已。
    如何实现公平锁 与 非公平锁?
    ReentrantLock 有内部类 sync,根据是否公平锁 ,有两种实现 FairSync 和 NonfairSync。

  • 非公平锁:线程在获取锁前有无查看过 AQS 队列里 有比自己更早请求该锁的线程,使用抢占策略,也就是 发现状态值为 0,就通过 CAS设置获取到锁。
        比方说,线程 A 先调用 lock() 方法,发现状态值不为0,而且当前线程也不是锁持有者,那就会被放入 AQS 阻塞队列;接下来 线程 B 也调用 lock() 方法,发现状态值为 0 了,就会通过 CAS 设置获取到锁,明明是 A 先请求锁的呀,确是 B 先获取到锁。
  • 公平锁:在 tryAquire() 方法中多了个 hasQueuedPredecessor()。只有队列为空 或者 当前线程是 是 队列里第一个元素 head,才会用 CAS 获取锁。这样就能保证 公平锁的 串行化 。

     不适合 写少读多的情况。

(4)ReentrantReadWriteLock 读写锁

     适合 写少读多的情况,读写分离。底层也是 AQS。
     ReentrantReadWriteLock 内部维护了一个 ReadLock【是静态内部类,有字段 Sync,Sync是AQS 的实现类】 和 一个 WriteLock【独占锁,可重入,排斥读锁】,依赖 Sync 实现功能,Sync 继承自 AQS,提供了 公平 和 非公平的实现。state 高 16 位表示 读状态,获取到读锁的次数;低 16 位表示 写状态,获取到写锁的线程 的 可重入次数。

 ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock(); 
//读锁为共享锁,允许多个线程进入临界区
 //但是如果写锁被某线程占用,则读锁获取失败,线程进入阻塞。

lock.readLock().unlock(); 
//释放读锁

lock.writeLock().lock();
 //写锁为独占锁,只能允许单个线程进入临界区(之后的代码块),
 //并且排斥读锁


lock.writeLock().unlock();
//释放写锁

三个同步器

(5)Semaphore 信号量锁

    内部的计数器是递增的,初始化时可以指定一个初始值,在需要同步的地方调用 acquire 方法时指定需要同步的线程个数。如主线程中Semaphore 的构造参数为 0,在 2 个子线程内部调用 信号量的 release 方法,相当于让计数器 +1,然后在主线程里调用 信号量 的 acquire 方法,传入参数 2 ,主线程就会阻塞,直到信号量的计数变为 2 才会返回。
    Semaphore 默认是非公平,可以通过构造方法指定为公平锁,比如:release() 方法:信号量+1,如果当前 有线程 因为调用 acquire() 方法被阻塞而放入了 AQS 的阻塞队列,则会根据公平策略选择一个信号量个数能满足的线程进行激活,激活的线程会尝试获取刚增加的信号量。

(6)CountDownLatch 共享锁

     适用于 主线程需要等待所有的子线程,比 join() 优雅。
     使用 AQS 的状态变量来存放计数器的值,首先在初始化时设置状态值,比方说主线程中启动 2 个子线程,计数器就设置为 2。当线程调用 await() 方法后 线程 即 主线程 会被阻塞,其他线程 即 子线程 要调用 countDown 方法,使用 CAS 使计数器值减 1 ,当计数器变为 0 时,当前线程会调用 AQS 的 doReleaseShared 方法来激活由于调用 await() 方法而被阻塞的线程 即 主线程。

(7)CyclicBarrier 回环屏障

    可以复用,适合于 分段任务 有序执行的情景。
    CountDownLatch 的计数器是一次性的 ,也就是说 等到计数器值 变为 0 后,再调用 CountDownLatch 的 await 和 countDown 方法就会立刻返回。为了满足 计数器重置的需要,有了这个 回环屏障。
    回环 是因为 当所有线程执行完毕,并重置 CyclicBarrier 的状态后,可以被重用。
    屏障 是因为 线程调用 await 【不是 Condition Object 的那个 await 】方法后就会被阻塞,这个阻塞点就会被称为 屏障点,等所有的线程都调用了 await() 方法后,线程们就会冲破屏障,继续向下运行。

举个例子,子方法要有锁,多次调用子方法,这样子方法用什么锁比较合适?

???

生产者、消费者模型实现 n、n 的阶乘

移步博客:https://blog.csdn.net/weixin_41750142/article/details/104939185

按锁的类型分类

乐观锁:由CAS实现的: 原子性操作类、Random、
悲观锁:synchronized、

公平锁与非公平锁:ReentrantLock、Semaphore
    默认是非公平锁。

并发包中对不同的数据结构的实现

(1)并发队列

(1)ConcurrentLinkedQueue 单链表,CAS,未加锁,size 不准确。
(2)ArrayBlockingQueue 有界数组,ReentrantLock 锁,(粒度比较大,读和写都需要获取到 锁)size 准确。
    offer、 pull 只用 lock、unlock ,而 put 、take 还用了 两个条件变量。
(3)PriorityBlockingQueue 无界队列,ReentrantLock 锁,CAS 实现扩容,因为是无界的,所以 put 时永远不会处于 await 状态,所以不需要被唤醒,只有一个条件变量 notEmpty。
(如果是有界的话,put 时,如果队列满,要把当前线程阻塞挂起放入 notFull 队列,poll 出队会调用 notFull 的 signal 方法唤醒 因 put 阻塞的线程。)
(4)LinkedBlockingQueue 有界单链表,默认长度 2 的 31 次方-1,可以自己指定容量,由 2 个 ReentrantLock 实例控制 入队 和 出队,(入队和出队是可以同时的)对应两个条件变量 放置被阻塞的线程。
(5)DelayQueue 无界,ReentrantLock 锁,内部有 优先级队列,每个元素都有个过期时间,当从队列获取元素时,只有过期元素才会出队。

(2)ConcurrentHashMap

    HashMap 是线程不安全的,而 Hashtable 所有的方法都加了 synchronized ,线程安全但效率不高,再有:Hashtable 的 hash 数组默认大小是11,扩容方式是 *2+1,散列方式也是直接使用对象的 hashCode 。

    高并发应该用 ConcurrentHashMap。
    内部是 HashEntry 数组,使用 volatile 保证内存可见性,分段锁设计,有一个 Segments 数组,元素是 Segment , Segment 继承了 ReentrantLock ,默认数量是 16,每个 Segment 里有多个 HashEntry,读时多个线程可访问,写时每个 Segment 只有一个线程可写入,可允许 16 个线程同时写入各自 Segment ,扩容也是对单个 Segment 进行扩容。
    要注意的是,ConcurrentHashMap 的 put 方法,如果 key 值已存在,会用 新 value 值覆盖,并返回 旧值,而如果不存在 key ,是要放入 value ,并返回 null ,而有个 putIfAbsent 方法,如果 key 存在 直接返回原来对应的值不用 value 覆盖,如果不存在,放入value,返回 null。

    JDK 8 版本 的 ConcurrentHashMap 相对于 7 版本,发生了很大改动,直接抛弃了Segment的设计,Node(数组,默认长度16,自动扩容,扩容速度 0.75) + CAS + Synchronized 设计,保证线程安全。
    每一个节点,挂载一个链表,当链表挂载数据大于8时,链表自动转换成红黑树。
    put 方法,如果当前位置为 null ,用 CAS 循环 更新的,如果不为 null ,需要链上节点或者转化成红黑树,是用 synchronized ,JDK1.8对 synchronized 做了优化,其执行性能已经跟 ReentrantLock 不相上下。

(3) CopyOnWriteArrayList 是怎么实现的?

    线程安全的 ArrayList,增、删、改都是在的 复制的数组 上进行的。用 ReentrantLock 独占锁对象来保证 同时只有一个线程操作。
    写时复制 存在 弱一致性 的问题,获取指定位置元素是先获取 array 数组,然后通过下标访问,而这个过程是没有加锁的,如果 线程A 获取到数组后,线程B 操作了数组,而线程 A 中数组仍是操作前的数组,通过下标访问的 自然也不是操作后的。

(4)CopyOnWriteArraySet

    内部有个 CopyOnWriteArrayList,不允许重复,调用 add 方法时,用 ReentrantLock 加锁,遍历元素,有重复元素直接返回 false,遍历完没有重复才在数组末尾添加。

今日补充

  • 联合索引的命中规则?
  • 介绍TCP/IP头,其中哪些首部涉及滑动窗口,以及拥塞控制
  • 线程池,使用情景
  • JDK1.8新特性?

底层实现系列

  • 说说 int 和 Interger 的区别,适合什么场景,为什么?Interge底层怎么实现的。
    答:int 是 基本数据类型,初始值是0;Integer是封装类,初始值是 null 。
        应用场景的区别:
        比如要体现出 考试成绩为 0 和 缺考 的区别的时候 用Integer可以 int不行;用容器的时候 ,ArrayList等职能放对象,不能放基本数据类型。

  • 说说泛型,底层怎么实现的知道吗?

  • 序列化底层是怎么实现的?

  • JSON?平常怎么使用的?说说底层实现?…

了解

  • 了解分布式锁吗?
 类似资料: