Loading

Interview-1

date: 2020-12-28 13:54:24
updated: 2021-01-19 20:55:32

Interview-1

1. 线程的状态

  • 新建(NEW):新建一个线程
  • 可运行(RUNNABLE):调用start,等待cpu的使用权
  • 运行(RUNNING):可运行(RUNNABLE)的线程拿到cpu时间片,开始执行
  • 阻塞(BLOCKED):指线程因为某种原因放弃了cpu 使用权,即让出了cpu时间片
    • 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中
    • 同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
    • 其他阻塞:运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
  • 终止(DEAD)

2. 死锁的现象和解决方法

死锁,是指多个进程循环等待它方占有的资源而无限期地僵持下去的局面。

死锁产生的四个必要条件:

  • 互斥:共享资源同时只能被一个线程访问
  • 占有且等待:线程T1在取得共享资源A的时候,请求等待资源B的时候并不释放资源A。
  • 不可抢占:其他线程不能强行抢占线程的资源。
  • 循环等待条件:线程T1在持有资源A1,同时在请求等待获取资源B,线程T2在持有资源B,然后在请求等待线程T1的持有资源,形成了交叉闭环申请。

解决方法:

  • 预防
    • 打破互斥条件。即允许进程同时访问某些资源。但是很多情况有些资源是不支持同时访问的。这个方法基本没用
    • 打破不可抢占条件。即允许进程强行从占有者那里夺取某些资源。当一个进程已占有了某些资源,它又申请新的资源,但不能立即被满足时,它必须释放所占有的全部资源,以后再重新申请。它所释放的资源可以分配给其它进程。相当于线程占有的资源被隐蔽强占了。这个方法实现困难,会降低性能
    • 打破占有且等待条件。可以实行资源预先分配策略。如果一个线程所申请的所需的全部资源得不到满足,则不分配任何资源,线程不运行。这个方法会降低线程的并发性
    • 打破循环等待条件,实行资源有序分配策略。需要提前对资源进行分类编号,增加系统开销
  • 避免
    • 允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次资源分配的安全性。若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,令进程等待。安全序列的定义:所谓系统是安全的,是指系统中的所有进程能够按照某一种次序分配资源,并且依次地运行完毕,这种进程序列{P1,P2,...,Pn}就是安全序列。如果存在这样一个安全序列,则系统是安全的;如果系统不存在这样一个安全序列,则系统是不安全的。
  • 检测与恢复
    • 通过检测算法建立线程等待图,如果出现环路则表示有死锁。
    • 通过抢占资源、回退执行、杀掉进程三种方式实现恢复

预防是排除死锁的静态策略,基本没啥用。避免是动态策略,是对线程申请资源这个操作加以限制。但是系统具有并发、共享和随机性等特点,所以也很难实现。主要是检测与恢复,能发现死锁并从死锁中恢复出来。

3. ConcurrentHashMap 的数量

由于调用方法的时候可能并发进行增删操作,所以这个数量只是一个估值。

一共有两种方式来获取数量,一个是 size(),如果数量大于int的最大值返回int最大值,根据JDK8注释来看,应该用 mappingCount() 来取代 size()。两个方法底层调用的都是 sumCount()

final long sumCount() {  
    CounterCell[] as = counterCells; CounterCell a;  
    long sum = baseCount;  // 实际上保存的是hashmap中的元素个数  利用CAS锁进行更新,但它并不用返回当前hashmap的元素个数
    if (as != null) {  
        for (int i = 0; i < as.length; ++i) {  
            if ((a = as[i]) != null)  
                sum += a.value; //所有counter的值求和  
        }  
    }  
    return sum;  
}

每一次put操作都会调用 addCount(),通过cas来累加 baseCount,然后查看是否需要扩容。在并发时如果利用CAS修改baseCount失败,会利用CAS操作修改CountCell的值。

4. cas

基于调用JNI代码实现,借助C来调用CPU底层指令实现。高效轻量,但是有三个问题

  • ABA:借助AtomicStampedReference来解决
  • 循环时间可能很长:
  • 只能保证一个共享变量的原子操作。可以将多个共享变量合并成一个,或者借助AtomicReference类保证引用对象之间的原子性,把多个变量放在一个对象里进行cas操作

5. concurrentHashMap 1.7 和 1.8 区别

  • 整体结构
    • 1.7:segment + hashEntry + unsafe
    • 1.8:移除 segment,使锁的粒度更小, synchronized + CAS + node数组 + unsafe
  • put
    • 1.7:先定位segment,再定位桶,全程加锁
    • 1.8:类似于HashMap,直接定位到桶,拿到first节点后进行判断,1、为空则CAS插入;2、为-1则说明在扩容,则跟着一起扩容;3、else则加锁put(类似1.7)

6. 如何在很短的时间内将大量数据插入到ConcurrentHashMap / 如何提高concurrentHashMap的插入效率

主要有两个地方消耗会比较大。一个是扩容操作,一个是锁资源竞争。扩容问题可以通过配置合理的容量大小和扩容因子,尽量减少扩容。锁资源争夺问题,主要是put时会对头结点加上synchronized,优化的一个点可以是锁的等级。将数据根据hash先分类,hash冲突的数据保存在一起,然后使用单线程来put,这样put锁的是同一个头结点,可以将锁控制在偏向锁级别。

synchronized 级别:偏向锁 -> 轻量锁 -> 重量锁。
偏向锁:偏指的是偏向于第一个访问的线程。在无竞争的环境下,有一个线程访问的同步代码块,这个锁就会偏向这个线程,下次访问的时候会检查对象头的mark word记录的是否是当前线程ID,如果是的话就会保持偏向锁,并执行同步代码块。但如果不是偏向锁,就会通过cas来替换线程ID,此时就可能会升级为轻量锁。

7. 线程池对工作线程的回收

线程池的创建主要是通过 ThreadPoolExecutor 这个类来创建的,Executors 类也可以创建,但是不能自定义,类型有限

线程启动后,就会进入到 runWorker(Worker w) 方法。里面是一个while循环,循环判断任务是否为空,若不为空,执行任务;若取不到任务,或发生异常,退出循环,执行 processWorkerExit(w, completedAbruptly); 在这个方法里把工作线程移除掉。

获取任务主要是 getTask() 方法(还有一个 firstTask,只会执行一次),在不考虑异常的场景下,返回null,就表示退出循环,结束线程。下一步,就得看看,什么情况下 getTask() 会返回null。

两种情况

  • 线程池的状态已经是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作队列为空
  • 工作线程数已经大于最大线程数或当前工作线程已超时,且,还有其他工作线程或任务队列为空。

当线程池中的线程数小于corePoolSize 时,新提交的任务直接新建一个线程执行任务(不管是否有空闲线程)
当线程池中的线程数等于corePoolSize 时,新提交的任务将会进入阻塞队列(workQueue)中,等待线程的调度
当阻塞队列满了以后,如果corePoolSize < maximumPoolSize ,则新提交的任务会新建线程执行任务,直至线程数达到maximumPoolSize
当线程数达到maximumPoolSize 时,新提交的任务会由(饱和策略)管理
任务就理解为生产者消费者中的同步队列

int wc = workerCountOf(c);
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

if ((wc > maximumPoolSize || (timed && timedOut))
    && (wc > 1 || workQueue.isEmpty())) {
    if (compareAndDecrementWorkerCount(c))
        return null;
    continue;
}

try {
    Runnable r = timed ?
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
        workQueue.take();
    if (r != null)
        return r;
    timedOut = true;
} catch (InterruptedException retry) {
    timedOut = false;
}

如果设置了 allowCoreThreadTimeOut(true) 或者当前运行的线程数大于核心线程数,timed = true 此时将从 workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) 队列里拿出来一个线程执行,如果在规定时间内没取到,就返回null;否则就会 workQueue.take(); 所以线程池里的线程一直在等待任务执行不被销毁是因为进入了阻塞状态

workQueue 一般是 arrayBlockingQueue(或者 linkedBlockingQueue),至少也是一个阻塞的,当篮子里没有消息的时候,也就是 getTask()

8. 数据库的脏读、不可重复读和幻读

  • 脏读:一个事务读取到另一个事务没有提交的数据。我们举个例子:事务A1修改了一行数据,但是还没有提交(还没写入硬盘),这时候事务A2读取了被事务A1修改后的数据,之后事务T1因为一些原因Rollback回滚了,那么事务T2读取的数据就是脏的。
    • 解决:事务隔离级别调整到READ_COMMITTED
  • 不可重复读:指的是同一个事务, 相同的查询过程读取出了不同的结果。比如说,事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
    • 解决:事务隔离级别调整到REPEATABLE_READ
  • 幻读:指的是事务不独立执行的时候,可能出现的错误。比如:A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样。这就叫幻读。
    • 解决:事务隔离级别调整到SERIALIZABLE_READ

隔离级别:

  • DEFAULT
  • READ_UNCOMMITTED:读未提交,即能够读取到没有被提交的数据,啥也解决不了,很少用
  • READ_COMMITED:读已提交,即能够读到那些已经提交的数据,自然能够防止脏读
  • REPEATABLE_READ:重复读取,即在数据读出来之后加锁,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录
  • SERLALIZABLE:串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务

9. TCP和UDP

  • TCP:面向连接,三次握手,传输可靠
  • UDP:传输数据前不需要建立连接,适合传输大量数据,但是不可靠,容易丢包,比如直播视频,丢帧就丢了

10. BIO、NIO、AIO & select/poll、epoll

举个生活中简单的例子,你妈妈让你烧水,小时候你比较笨啊,在哪里傻等着水开(同步阻塞)。等你稍微再长大一点,你知道每次烧水的空隙可以去干点其他事,然后只需要时不时来看看水开了没有(同步非阻塞)。后来,你们家用上了水开了会发出声音的壶,这样你就只需要听到响声后就知道水开了,在这期间你可以随便干自己的事情,你需要去倒水了(异步非阻塞)。

  • BIO:同步阻塞。通过线程池的方式来对每一个请求提供应答,但是无法应对海量的请求并发
  • NIO:同步非阻塞。在Java 1.4 中引入了NIO框架,对应 java.nio 包,提供了 Channel , Selector,Buffer等抽象。
    • NIO 和 BIO 的区别:
      • IO流是阻塞的,NIO流是非阻塞的
      • IO面向流,而NIO面向缓冲区。所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了ByteBuffer,还有其他的一些缓冲区,事实上,每一种Java基本类型(除了Boolean类型)都对应有一种缓冲区。
      • NIO 通过Channel(通道) 进行读写。通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和Buffer交互。因为 Buffer,通道可以异步地读写。
      • NIO有选择器,而IO没有。选择器用于使用单个线程处理多个通道。避免了线程之间的切换,可以直接将线程通过select挂在到不同的channel下
    • NIO 读写数据的方式
      • 从channel读取:创建一个缓冲区,请求通道读取数据
      • 从channel写入:创建一个缓冲区,填充数据,并要求通道写入数据

    NIO 底层是由 epoll 实现,但是空轮询会导致cpu飙升 ==> netty

  • AIO:异步非阻塞。协程

  • 同步阻塞IO:请求数据的线程会一直阻塞,知道准备好数据
  • 同步非阻塞IO:用户态请求数据,如果内核态数据没准备好,用户态的线程可以去做别的,但是会不断询问数据是否准备好
  • 多路复用IO(NIO):用户态线程不断去询问,消耗CPU资源 ==> 有三种方式(select、poll、epoll),根本思想是将用户态的不断轮询交给内核态去做,其中selcet\poll是轮询的方式,epoll不是轮询。内核态准备好数据后,也不需要只找那个请求数据的线程,找其他用户态等着的线程来处理接下来的事情就好。
    • select:把需要的数据fd放到一个set里,因为是set所以有数量限制,在内核态不断轮询。缺点是线性轮询,效率低;用户态和内核态的复制非常消耗资源
    • poll:将set替换为链表的方式,这样就没有数量限制
    • epoll:示例如Nginx。不像前两个把fd放到一个集合里遍历,而是采用注册回调函数,在fd准备好的时候触发回调函数,把fd放到就绪队列,直接返回,这样就避免了遍历所有的fd,时间从O(n)变到O(1)

11. linux下的java进程和linux线程有一一对应的关系吗

如果线程有50%的时间被阻塞,线程的数量就应该是内核数量的2倍。如果更少的比例被阻塞,那么它们就是计算密集型的,则需要开辟较少的线程。如果有更多的时间被阻塞,那么就是IO密集型的程序,则可以开辟更多的线程。于是我们可以得到下面的线程数量计算公式:

线程数量=内核数量 / (1 - 阻塞率)

12. JVM 哪一些是线程独占哪一些是线程共享

  • 线程独占:栈、本地方法栈、程序计数器

  • 线程独享:堆、方法区

  • 栈:线程在执行每个方法时都会同时创建一个栈帧,用来存储局部变量表、操作栈、动态链接、方法出口等信息。调用方法时执行入栈,方法返回时执行出栈。

  • 本地技术栈:和Native方法有关

  • 程序计数器:PC寄存器,每个线程都有自己的程序计数器。倘若当前执行的是 JVM 的方法,则该寄存器中保存当前执行指令的地址;倘若执行的是native 方法,则PC寄存器中为空。

  • 堆:所有线程共享,所有的对象和数组都在堆上进行分配。

  • 方法区:主要用于存储类的信息、常量池、方法数据、方法代码等

13. JDK8 永久代和元空间

永久代(PermGen space):指的是方法区,但是和方法区有本质的区别。方法区是JVM的规范,永生代是JVM规范的一种实现,并且只有 HotSpot 才有永生代。由于方法区主要存储类的相关信息,所以对于动态生成类的情况比较容易出现永久代的内存溢出。最典型的场景就是,在 jsp 页面比较多的情况,容易出现永久代内存溢出。

JVM 是一种规范,hotspot是虚拟机的一种实现方式。openjdk项目包括hostspot这个组件

在JDK8中,HotSpot 已经没有 “PermGen space”这个区间了,取而代之是一个叫做 Metaspace(元空间) 的东西。

元空间(Metaspace):7将永久代移动到了堆(比如字符串常量移到了堆),8已经不存在永久代了。元空间的本质和永久代类似,都是对方法区的一种实现,但是最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此默认情况下,元空间的大小仅受本地内存限制,但是可以通过参数来指定元空间的大小。

-XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
-XX:MaxMetaspaceSize,最大空间,默认是没有限制的。

除了上面两个指定大小的选项以外,还有两个与 GC 相关的属性:
-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
-XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

14. Linux 分段

15. 抽象类和接口

JDK8 允许在接口中定义 static 方法和 default 方法,其中default方法可以选择重写;static 方法比如通过接口类调用,原因是一个类可能实现两个接口,如果两个接口类中的static方法一样,就会发现错误,所以需要指定接口类来调用static方法

抽象类就是对一类事物的共性东西提取出来;接口则是一种规范,许多情况下接口可以代替抽象类,毕竟Java单继承多实现

16. synchronized 和 ReentrantLock的区别

synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:

  • ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
  • ReentrantLock可以获取各种锁的信息
  • ReentrantLock可以灵活地实现多路通知

另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。

17. 怎么唤醒一个阻塞的线程

如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。

18. int和Integer的区别,为什么有了int还要有设计Integer

区别:

  • int 是基本数据类型, Integer 是包装类
  • Integer 必须实例化后才能使用
  • Integer 实际是对象的引用,指向new出来的对象;int 是直接存储数值
  • Integer 默认值是null, int默认值是0

深入:

  • 两个通过new生成的Integer变量永远是不相等的。因为new生成的是两个对象,其内存地址不同。
  • Integer与new Integer不会相等。因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同。
  • 两个都是非new出来的Integer,如果数在-128到127之间,则是true,否则为false。
    • java在编译Integer i = 127的时候,被翻译成 Integer i = Integer.valueOf(127); java API中对Integer类型的valueOf的定义如下,对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127这个Integer对象进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了。
  • Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true。(因为包装类Integer和基本数据类型int比较时,java会自动拆箱为int,然后进行比较,实际上就变为两个int变量的比较)

对象封装有很多好处,可以把属性也就是数据跟处理这些数据的方法结合在一起,比如Integer就有parseInt()等方法来专门处理int型相关的数据。
另一个非常重要的原因就是在Java中绝大部分方法或类都是用来处理类类型对象的,如ArrayList集合类就只能以类作为他的存储对象,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。所以Integer的存在是很必要的。

19. Executor封装的四种线程池类型

  • newCachedThreadPool:可缓存的线程池,最大线程数无上限(int最大值)
  • newFixedThreadPool:固定大小的线程池,所有的线程都是核心线程,没有空闲等待时间
  • newSingleThreadExecutor:单线程的线程池,相当于单线程串行执行所有任务
  • newScheduledThreadPool:最大线程数无上限,可设置核心线程数。此线程池支持定时以及周期性执行任务的需求。

20. 线程池为什么要使用阻塞队列而不使用非阻塞队列?

阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。

21. 常用的并发工具类有哪些?线程之间的互相通信

  • CountDownLatch
    • 就是一个线程等待,直到它所等待的其他线程都执行完成并且调用 countDown() 方法发出通知后,当前线程才可以继续执行
    //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
    public void await() throws InterruptedException { };   
    //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
    public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
    //将count值减1
    public void countDown() { }; 
    
  • CyclicBarrier 是所有线程都在等待,一直到所有线程都准备好进入 await() 方法之后,所有线程同时开始执行
  • Semaphore
  • Exchanger
  • wait\notify\notifyAll

CyclicBarrier 和 CountDownLatch 的区别

  • CountDownLatch 只能计算一次,但是 CyclicBarrier 可以 reset
  • CyclicBarrier 还提供了其他方法,比如 getNumberWaiting 方法可以获得阻塞的线程个数

22. volatile

  1. 保证可见性
  2. 禁止重排序。为了提高性能,系统自动重排序,但是 volatile 这条指令的位置是不可以被改变的。 1 2 volatile 3 4 1和2一定在volatile前面,3和4一定在volatile后面,但是1和2、3和4之前的顺序不能做保证,即可以做重排序

23. AQS

AQS解析

AQS 是一个用来构架锁和同步器的框架,支持三种同步方式

  • 独占式
    • 如 ReentrantLock
  • 共享式
    • 如 CountDownLatch
  • 组合式
    • 如 ReentrantReadWriteLock。ReadWriteLock 主要是为了读写锁分离,读锁是共享的,写锁是独占的

24. synchronized 中的锁池和等待池

JVM会为一个使用内部锁(synchronized)的对象维护两个集合,Entry Set 和 Wait Set

  • Entry Set: 如果线程A已经持有了对象锁,此时如果有其他线程也想获得该对象锁的话,它只能进入Entry Set,并且处于线程的BLOCKED状态。
  • Wait Set: 如果线程A调用了wait()方法,那么线程A会释放该对象的锁,进入到Wait Set,并且处于线程的WAITING状态。

对于Entry Set中的线程,当对象锁被释放的时候,JVM会唤醒处于Entry Set中的某一个线程,这个线程的状态就从BLOCKED转变为RUNNABLE。

对于Wait Set中的线程,当对象的notify()方法被调用时,JVM会唤醒处于Wait Set中的某一个线程,这个线程的状态就从WAITING转变为RUNNABLE;或者当notifyAll()方法被调用时,Wait Set中的全部线程会转变为RUNNABLE状态。所有Wait Set中被唤醒的线程会被转移到Entry Set中。

24.1 wait()方法外面为什么是while循环而不是if判断

普遍是多个线程生产,多个线程消费,如果用 if() 还有可能出现:当一个生产者放入数据后,两个消费者都 if 判断通过,然后过度消费的情况,同理过度生产情况。

24.2 为什么要用notifyAll()方法,用notify()行吗

c1 c2 拿到锁,发现队列是空,全都 wait()
p1 拿到锁,生产,notify()
但是此时有可能 p2 也在 Entry Set 里等锁,p2 拿到锁,此时队列是满的,需要 wait(),此时 c1 c2 p2 都在等
如果这时候 c1 消费完了,notify(),如果这时候唤醒 p2 没问题,但如果唤醒 c2 会继续等待,万一 p1 不再生产,那么 c2 p2 就会互相等待,造成死锁 => 使用 notifyAll() 的原因就是 notify() 非常容易导致死锁

25. sleep 和 wait 的区别

如果线程持有某个对象的监视器,那么sleep不会放弃这个对象的监视器,而 wait 会放弃这个监视器。其实就是锁,sleep 不会释放锁

26. 线程类的构造方法、静态块是被哪个线程调用的

线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用

假设 Thread2 中 new 了 Thread1,main 函数中 new 了 Thread2,那么

  1. Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run() 方法是Thread2 自己调用的
  2. Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run() 方法是Thread1 自己调用的

27. 设计模式

IOC的工厂模式、bean单例模式、装饰者模式、AOP动态代理等。
nio:反应器模式
redis:基于Reactor模式

28. Redis 线程模型

每一个socket对应的fd会注册到epoll中,epoll会监听哪一个socket发送了命令,然后命令会进入队列,被顺序执行。整个过程只在调用select、poll、epoll的时候才会阻塞,socket不会阻塞,收发客户消息不会阻塞,这就是事件驱动,也就是 reactor 模式。

30. 并发修改数据库并回写Redis 如何保证数据一致性

31. redis的hash怎么实现的,rehash过程讲一下 和JavaHashMap的rehash有什么区别?redis cluster怎么做到高可用的?

32. raft算法的基本流程?raft算法里面如果出现脑裂怎么处理?

33. paxos和zookeeper的zab算法,他们之前有啥区别?

34. 删除链表的倒数第 N 个节点

双指针,first 第一次先移动N个节点,second移动一个节点,之后first和second都移动一个节点,直到first.next=null,删除second对应的节点

35. 树

B-(B)树 多路搜索树
b+树在b树基础上,叶子结点增加链表指针,非叶子结点是叶子节点的索引
b*树在b+树基础上,非叶子结点也增加链表指针

二叉树最坏情况会变成线性结构,时间是O(n)。avl 二叉平衡树要求左右子树的最大高度不超过1,通过自旋来保证结构,从而保证时间是O(logn)
红黑树属于不严格的avl,不严格指的是不用严格去控制高度,这就保证了插入效率的提高。而查找的话,由于avl更平衡,所以avl的效率要比红黑树平均要高。红黑树算是空间换时间

B+树更适合做数据库索引的原因:磁盘读写代价更低,b树所有节点都有数据,导致要查找的时候一页数据更少;b+树的查询效率更稳定,任何查询都需要从根走到叶子
B树在提高了IO性能的同时并没有解决元素遍历效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。
B+树的缺点:会产生大量的随机IO。原因是随着数据的插入,逻辑上连续的叶子节点往往会在物理上不连续,无法甚至分隔很远,导致在做范围查询的时候,会产生大量的随机IO => 主要是由于数据全在磁盘,如果数据相距很远,磁盘寻址很慢

LSM树(Log-Structured Merge-Trees):把一颗大树拆分成n棵小树,先写入内存,在内存中构建一颗有序的小树,在到达一定大小后刷到磁盘。读数据的时候,由于不知道数据在哪棵小树,所以需要遍历所有的小树,每棵小树都是有序的 => 牺牲了部分读性能,大幅提高写性能
由此推到HBase的一些概念:

  • WAL:为了避免因为断电导致内存中的数据会丢失,所以先在磁盘上记录logfile,然后写到内存中,当数据刷到磁盘后,logfile就可以删除了
  • memstore、storeFile:内存中的小树就是memstore,每次flush,内存中的memstore就会变成磁盘上的storeFile
  • compact:小树过多,会影响到读的性能,所以需要不定时合并小树

hbase 使用跳表来保证内存memstore中key是有序的
MemTable 是内存中的数据结构,当MemTable到一定大小后,会转化成Immutable MemTable,这是将 MemTable 转成 SSTable 的一种中间状态。写操作会由新的 MemTable 处理。
SSTable 是有序键值对集合,是LSM树在磁盘中的数据结构,为了加快读取,可以建立索引以及布隆过滤器来加快key的查找

36. mysql 的索引

innoBD 引擎,主键是聚簇索引,如果不指定主键,将第一个列都是not null的唯一索引作为聚簇索引;每个innnoDB有且仅有有一个聚簇索引
再创建的都是非聚簇索引,在叶子节点会记录主键的值,然后指向聚簇索引的叶子节点
整个过程从K索引树到主键索引树的过程叫做回表

主索引(聚簇索引)叶子节点会保存整条数据,辅助索引(非聚簇索引)会保存指定的列+主键的数据,如果查询的时候列大于辅助索引中的列,那么就会根据查询到的id去主索引里继续查,也就是回表

37. 处理海量数据思路

  1. 分而治之/hash映射 + hash统计 + 堆/快速/归并排序
  2. 双层桶划分
  3. Bloom filter/Bitmap
  4. Trie树/数据库/倒排索引
  5. 外排序
  6. 分布式处理之Hadoop/Mapreduce

38. 一个线程死循环,另一个线程还能不能执行

要看死循环的线程占用的是什么锁,如果是自己的锁,那没事,另一个线程还可以被执行到;但如果两个线程占用的是同一个对象的锁,一个死循环,那另一个就执行不了

39. Kafka中的ISR、AR又代表什么?ISR的伸缩又指什么

ISR:In-Sync Replicas 副本同步队列
AR:Assigned Replicas 所有副本
ISR是由leader维护,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。

40. Yarn的调度方式

  1. 先进先出
    • 有可能会导致大量的小作业得不到执行
  2. 容器
    • 先划分一部分资源看做一个容器,专门用来运行小作业,缺点就是为小任务专门设置一个队列会预先占用一定的集群资源,这就导致大任务的执行时间会落后于使用FIFO调度器时的时间
  3. 公平
    • 只有一个job的时候,占用所有的资源;提交第二个job,会平分资源,使两个job共享集群资源

41. kafka 的数据积压

积压就是生产者速度 > 消费者速度
解决思路:

  1. 重启作业从上一次提交的offset开始重新执行
  2. 有可能是因为partition不够,导致数据过于集中,那么可以尝试增多partiton
  3. 减少每一次batch的数据量大小,略微提高获取batch的频率,勤拿少取
  4. 有可能存在数据倾斜,单纯是数据其中在某些partition里,那么就尝试在key前面加上随机数,打散数据

42. 在hbase表中插入大量数据

Region会不断增大,不停的split,会影响效率 => 进行分区,控制region的个数 => split 或者 加盐

43. hive 中的 union和union all

union 是 union distinct 的缩写,使用 union 的话会对select出来的列中的重复值进行去重并排序

union all 就是单纯把列的结果合并在一起,不去重不排序

44. 十台机器同时去执行一条命令

list.txt 中去写入一些机器的ip

for i in `cat ./allhosts`; do echo $i; ssh $i "service ntpd restart";done

45. flink中的 dataset 和 datastream

  • dataset 中的 source 来源于文件、表或者 Java 集合
  • datastream 中的 source 一般是消息中间件比如 Kafka

46. 咆哮位图

N/65536 = block_id
N%65536 = 在对应的block内的一个偏移量

将位图分block,每一个值都放到对应的block的位置上

47. HDFS 小文件

  1. 如果是hive的话
hive.merge.mapfiles
hive.merge.mapredfiles
hive.merge.size.per.task
hive.merge.smallfiles.avgsize
  1. 如果是已有的小文件

Hadoop 自带三个方案,Hadoop Archive,Sequence file和CombineFileInputFormat。

  • Hadoop Archive:主要是用来归档,如果其中小文件出现问题,需要重新归档,因为中间涉及到索引信息的更改等;另外如果作为mr的输入路径,那么还是一个小文件对应一个map
  • Sequence file:可以做压缩;支持splitable,那么就可以作为mr的输入;但是需要自己编写程序来实现,另外由于是二进制文件,不方便查看
  • CombineFile:也是基于mr来进行转换,如果要合并的小文件很多,那么最终合并的文件会包含过多的额外信息,浪费过多的空间,所以这种方案目前相对用得比较少

49. hive参数调优

set hive.exec.parrallel=true; # 针对类似union all这样的,操作相互不关联的情况,可以设置并行度
set hive.exec.parallel.thread.number=8; # 并行度的个数

set hive.map.aggr=true; # 在map端进行聚合
set hive.groupby.mapaggr.checkinterval=100000; # 在map端进行聚合操作的数据条数

# 在数据分布不均时,即发生倾斜时进行负载均衡,可以进行如下的参数设置
set hive.groupby.skewindata=true; # 生成的查询计划会有两个MR Job。第一个MR Job 中,Map 的输出结果集合会随机分布到 Reduce 中,每个 Reduce 做部分聚合操作,并输出结果,这样处理的结果是相同的 Group By Key有可能被分发到不同的Reduce 中,从而达到负载衡的目的;第二个 MR Job 再根据预处理的数据结果按照 Group By Key 分布到 Reduce 中,最后完成最终的聚合操作 # 类似于mapreduce中加一个combine

# join出现数据倾斜时,需要设置参数
set hive.skewjoin.key=10000; # 这个是join的键对应的记录条数超过这个值则会进行分拆
set hive.optimize.skewjoin=true; # 如果是join 过程出现倾斜 应该设置为true

set mapred.max.split.size=xx; # map前合并小文件
set hive.merge.mapfiles=true; # map结束后合并小文件
set hive.merge.mapredfiles=true; # 合并reduce输出文件

50. NameNode 单点故障

HDFS1 是一个namenode,一个secondarynamenode,元数据放在内存里,会出现单点问题和内存受限问题

两个NameNode,会去竞争ZK中的一把锁,谁抢到了谁就是active节点,类似于kafka的controller和follower,controller主要是为了同步元数据,然后follower去同步,所以kafka每一个broker都会有元数据

每个NameNode都有一个进程 ZKFC,会向zk汇报心跳情况,如果挂了之后,会自动做切换
HDFS的元数据会单独去建一个集群,里面会有2N+1个节点,叫 journalNode,它们会保存元数据信息,能保证这些 journalNode 里的数据都是一样的。
Active NameNode 往 journalnode 写元数据,Standby NameNode 从 journalnode 读元数据同步

51. presto & kylin

  • presto
    主要的处理都在内存中,拿一部分数据放内存计算,计算完之后,再拿下一部分数据
    如果涉及到多表关联的话,可能内存吃不消
  • kylin
    核心是Cube,cube是一种预计算技术,基本思路是预先对数据作多维索引,查询时只扫描索引而不访问原始数据从而提速。
    如果要查询的条件变化太多,没有命中,就会导致计算非常慢;同时导入数据后会需要重新去做cube

52. HiveSql报错

MapReduce
FAILED: Execution Error, return code 1 from org.apache.hadoop.hive.ql.exec.mr.MapRedTask

修改hive依赖的hdfs参数:
dfs.client.block.write.locateFollowingBlock.retries = 8
dfs.client.block.write.retries = 8
dfs.client.cached.conn.retry = 5

Hive on Spark
FAILED: Execution Error, return code 1 from org.apache.hadoop.hive.ql.exec.spark.SparkTask
和上面一样,可能最底层的报错原因都是:Unable to close file because the last block does not have enough number of replicas。这说明了在某一时刻可能有任务大量读取blocks,耗费了过多的资源。

解决:

  1. 排查出读取blocks过多的任务,对该任务进行优化调整,缩短读取的时间范围或通过中间表的形式进行查询,尽可能的不要一次读取过多blocks。
  2. 修改hive的参数 同上

异常日志定位
1、查看Hiveserver2 日志, 使用 return code 找到出现该异常日志的线程编号
2、基于线程编号以关键字 Thread-28017]: Running with YARN Application 找到 applicationid
3、使用yarn logs -applicationId xx 获取到任务的所有日志信息
4、基于关键字 YarnAllocator: Driver requested 找到 spark 任务的 driver
5、在driver 日志中用关键字 ERROR, 寻找异常信息, 定位出异常的executor
6、找到executor 后, 基本就能找到出错原因

posted @ 2021-02-07 15:48  猫熊小才天  阅读(202)  评论(0编辑  收藏  举报