Java并发编程面经大全——双非上岸阿里巴巴系列

Posted 来老铁干了这碗代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程面经大全——双非上岸阿里巴巴系列相关的知识,希望对你有一定的参考价值。

东北某不知名双非本科,四面成功上岸阿里巴巴,在这里把自己整理的面经分享出来,欢迎大家阅读。


序号文章名超链接
1操作系统面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门1
2计算机网络面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门2
3Java并发编程面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门3
4Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列2021最新版面经——>传送门4
5面试阿里,你必须知道的背景知识——双非上岸阿里巴巴系列2021最新版面经——>传送门5

本博客内容持续维护,如有改进之处,还望各位大佬指出,感激不尽!



1. 基础知识

1. 并发编程的优缺点

1. 并发编程的优缺点

优点

  • 充分利用多核CPU的计算能力
  • 方便进行业务拆分

缺点

会出现内存泄漏、上下文切换、线程安全、死锁等安全问题。

2. 并发三要素

原子性:多个操作要么全部执行成功,要么全部执行失败

可见性:一个线程对共享变量的修改,另一个线程能够立刻看到

有序性:程序执行的顺序按代码的先后顺序执行。

出现线程安全问题的原因:

  • 线程切换带来的原子性问题
  • 缓存导致的可见性问题
  • 编译优化带来的有序性问题

解决办法:

  • JDK Atomic开头的原子类,synchronized、LOCK、可以解决原子性问题
  • sync、volatile、lock,可以解决可见性问题
  • volatile、Happens-Before规则可以解决有序性问题。

3. 进程和线程的区别

根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行

1.1 进程、线程和协程

进程是对运行时程序的封装,是系统进行资源调度和分配的的基本单位,实现了操作系统的并发;
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的 实时性,实现进程内部的并发;
一个程序至少有一个进程,一个进程至少有一个线程,线程依赖于进程而存在;
进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。

协程:相较于线程更加轻量级,不被操作系统内核管理,完全是由程序所控制。

协程在子程序内部是可中断的,转而执行别的子程序,在适当的时候再返回来接着执行。

好处

1、性能提升,不会像线程切换那样消耗资源。

2、不需要多线程锁的机制,因为只有一个线程,不存在同时写变量冲突。因此在协程中控制共享资源不加锁,只需判断状态,因此执行效率比线程高很多。

2 进程有哪几种状态?

就绪状态:进程已获得除处理机以外的所需资源,等待分配处理机资源;
运行状态:占用处理机资源运行,处于此状态的进程数小于等于CPU数;
阻塞状态: 进程等待某种条件,在条件满足之前无法执行;

3 进程间的通信的几种方式

管道(pipe)及命名管道(named pipe):管道可用于具有亲缘关系的父子进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;
信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息;
共享内存:可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等;
信号量:主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段(生产者消费者);
套接字:这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

4 线程有几种状态?

  1. 新建(NEW):新创建了一个线程对象。
  2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
  3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
  4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。阻塞的情况分三种:
    (一). 等待阻塞:运行(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)状态。
  5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

5 创建线程的方式

  • 继承Thread类

    //定义一个继承Thread类的Runner类
    class Runner extends Thread {
        //定义线程所拥有的方法
        public void run (){
            System.out.println("线程创建完毕");
        }
    }
    
    Runner r = new Runner();
    r.start();
    //如果使用r.run()仅相当于方法的调用
    
  • 实现Runnable接口

    //定义一个实现Runnable接口的类
    class MyThread implements Runnable {
        public void run (){
            System.out.println("创建成功");
        }
    }        
    //创建MyThread的对象,并用这个对象作为Thread类构造器的参数
    MyThread r = new MyThread();
    Thread t = new Thread(r);
    t.start();
    
  • 实现Callable接口

    做法:首先重写Call方法,启动线程时,需要新建一个Callable的实例,再用Future Task实例包装它,最终再包装成Thread实例,调用start方法启动。

    //创建线程的方式:Callable
    class MyThread implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            System.out.println(Thread.currentThread().getName()+"\\t coming in");
            TimeUnit.SECONDS.sleep(3l);
            return 1024;
        }
    }
    
    //用Callable的方式创建线程,相比较于Runnable,可以获取返回的值
    MyThread thread = new MyThread();
    //需要一个FutureTask,而且在两个线程里传入同一个futureTask,只会执行一次
    FutureTask<Integer> futureTask = new FutureTask<>(thread);
    new Thread(futureTask,"A").start();
    
  • 使用Executors工具类创建线程池

    1. newFixedThreadPoo:固定大小线程池
    2. newCachedThreadPool:带缓冲线程池
    3. newSingleThreadExecutor:单线程线程池
    4. newScheduledThreadPool:任务调度线程池
    5. ForkJoinPool:分治线程池
public class t {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        MyRunnable runnableTest = new MyRunnable();
        for (int i = 0; i < 5; i++) {
            // 每次循环,都创建一个线程
            executorService.execute(runnableTest);
        }
        System.out.println("线程任务开始执行");
        // 当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务(串行执行)
        executorService.shutdown();
    }

}
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("执行中");
    }
}

6 Runn\\Callable,Future (Task)

相同点:

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程

不同点

  • Runnable接口run方法无返回值。 Callable有,被线程执行后,可以返回值,这个返回值可以被Future拿到。

    Future表示异步任务,是一个可能还没有完成异步任务的结果。

    因此:Callable用于产生结果,Future用于获取结果

    FutureTask:表示异步运算任务,里面可以传入Callable具体实现类,可以对这个异步运算任务的结果进行等待获取,判断是否已经完成,取消任务等操作。

  • Runnable接口run只能抛出异常时运行,且无法捕获处理;Callable接口call方法允许抛出异常,且可以获取异常信息。

7 run和start区别

  • start()方法用于启动线程,run()方法用于执行线程的运行时代码,叫做线程体。

  • run()只是一个线程里的普通函数,可以重复调用,而start()只能调用一次

总结:调用start方法可以启动线程并使线程进入就绪状态,而run方法知识thread的一个普通方法调用,还是在主线程里执行。

8 守护、用户线程

用户线程:运行在前台,执行具体任务,如程序的主线程,连接网络的子线程等都是用户线程。

守护线程:运行在后台,为其他前台线程服务。一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作。(如垃圾回收线程)

*9 查看线程运行的方法

1. Windows

  • 任务管理器查看or杀死

  • cmd->tasklist + taskkill(杀死进程)

    jps
    taskkill /F /PID 33736 (F是强烈杀死)
    

2. linux

  • ps -fe 查看所有进程
    • ID、当前状态、启动时间、占用CPU时间、CPU占用百分比、内存占用百分比、占用虚拟内存的大小、占用常驻内存的大小
  • ps -fT -p <PID> 查看某个进程(PID)的所有线程
  • kill杀死进程
  • top 按大写 H 切换是否显示线程
  • top -H -p <PID>查看某个进程(PID)的所有线程

3. Java

注意:需要到对应的java\\lib下面运行

  • jsp查看所有Java命令

  • jstack<PID>查看某个Java进程(PID)的所有线程状态

  • jconsole查看某个Java进程中线程的运行情况(图形界面)

    • 先用如下方式运行你的Java类

      java -Djava.rmi.server.hostname=ip地址` -Dcom.sun.management.jmxremote -
      
      Dcom.sun.management.jmxremote.port=`连接端口` -Dcom.sun.management.jmxremote.ssl=是否安全连接 -
      
      Dcom.sun.management.jmxremote.authenticate=是否认证 java类`
      
      ip地址  连接端口(12345) false flase test.java
      
    • 远程连接:

在这里插入图片描述

  • 关闭防火墙:service iptables stop

10 线程同步的方式

互斥量 Synchronized/Lock:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
信号量 Semphare:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
事件(信号),Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作

11 什么是上下文切换

多线程编程中一般线程的个数都大于CPU核心的个数。

但一个线程时间片用完后,会重新处于就绪状态并让给其他线程使用,这个过程属于一次上下文切换。

概括:当前任务在执行完CPU时间片切换到另一个任务前会保存自己的状态,以便下一次再切换回这个任务时,可以再加载这个任务的状态。

上下文切换是计算密集型的。耗时。

Linux相比其他操作系统的优点之一是:上下文切换和模式切换的时间消耗很少。

12 如何减少上下文切换

多线程竞争时,会引起上下文切换。

  • 无锁并发编程:用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同线程处理不同段数据。

  • CAS算法:Java的Atomic包使用CAS算法来更新数据,而不需加锁

  • 使用最少线程:避免创建不需要的线程

  • 协程:在单线程里实现多任务的调度,维持多任务间的切换。

13 线程调度策略

分时调度模型、抢占式调度模型。

JVM采用抢占式调度模型。

14 线程调度器

线程调度器是一个操作系统服务,负责为Runnable状态的线程分配CPU时间,一旦我们创建了一个线程并启动它,它的执行便依赖于线程调度器的实现

15 你是如何调用wait()的?

应在循环中调用。当进程获取到CPU开始执行时,其他条件可能还未满足

16 为什么wait,notify定义在Object中

  1. 在Java的内置锁机制中,每个对象都可以成为锁,也就是说每个对象都可以去调用wait,notify方法,而Object类是所有类的一个父类,把这些方法放在Object中,则Java中的所有对象都可以去调用这些方法。
  2. 一个线程可以有多个对象锁,因此统一把对象锁设置为Object,这样Jvm就会很容易知道应该从哪个对象锁的等待池中唤醒线程。否则它根本不知道要操作的是哪一个

17 为什么wait,notify必须在同步方法or同步块中被调用?

当一个线程调用wait时,它必须拥有该对象锁,随后释放并等待,若达到了notify后,再进入锁。 由于所有的方法都需要线程持有锁,只有就只能通过公平来实现,所以他们只能在同步方法或同步快中被调用。

18 yield()和Sleep区别

(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;

(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;

(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。

19 如何停止一个线程

  1. 使用退出标志,也就是当run结束后线程终止
  2. 使用stop方法,但不推荐
  3. 使用interrupt方法

20 为什么Sleep和yield是静态的

sleep和yield只是在当前执行的线程上运行,因此在其他处于等待状态的线程上调用这些方法没有意义。

而静态是独一份,只要被调用,一定是在正在执行的线程中工作。 避免程序员错误的认为可以在其他非运行线程调用这些方法。

21 同步方法和同步块哪个更好

同步块是更好的选择,因为它不会锁住整个对象。同步方法会锁住整个对象。 同步的范围越小越好。

22 如果提交时,线程池队列满,会发生什么

这里区分一下:

(1)如果使用的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务

(2)如果使用的是有界队列比如 ArrayBlockingQueue,任务首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy

23 Servlet线程安全吗

非线程安全,是单实例多线程的。

24 如何保证多线程安全

1、使用安全类,如juc下的类,or使用原子操作,AtomicInteger

2、使用自动锁synchronized

3、使用手动锁Lock

2. 并发

1. 若对象置null,垃圾回收器是否立即释放对象占用内存

不会,在下一个垃圾回调周期中,这个对象是被可回收的。

2. finalize()什么时候被调用

当GC决定回收某对象时,就会调用finalize方法

3 as-if-serial & happens-before

语义

as-if-serial:不管如何重排序,单线程程序的执行结果不能被改变

happens-before:

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

二者区别

  • as-if-serial语义保证单线程内程序执行结果不被改变。

  • happens-before关系保证正确同步的多线程程序的执行结果不被改变


  • as-if-serial:让程序员误以为单线程程序是按程序顺序执行的(其实可能不是,只要结果一样)

  • happens-before:让程序员误以为多线程程序是按happens-before指定的顺序执行的。


  • 二者的目的都是为了在不改变程序执行结果的前提下,尽可能提高程序执行的并行度。

3. 并发关键字

synchronized

1. synchronized作用:

控制线程同步、重量级锁、效率低

1.6及以后引入大量优化,如自旋锁,适应性自旋锁,锁消除,锁粗化,偏向锁,轻量级锁等减少锁开销

2. 方法锁、对象锁、类锁

方法锁:每个类对象对应一个锁,当对象中某方法被synchronized修饰后,若别的线程使用同一对象调用该方法就会被阻塞

public synchronized void a() {
    //...
}

对象锁:与方法锁同理,不过针对的是一个代码块

public void a() {
	synchronized(this) {
		//...
	}
}

类锁:即使不同的对象调用这个方法或代码块,也会产生阻塞。力度更大。

//第一种定义方法
public static synchronized void a() {
	//...
}
//第二种定义方法
public void a() {
	synchronized(b.class) {	//b是类
        //...
    }
}

6、什么是死锁?死锁产生的条件?

1). 死锁的概念

在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲,就是两个或多个进程无限期的阻塞、相互等待的一种状态。

2). 死锁产生

互斥:至少有一个资源必须属于非共享模式,即一次只能被一个进程使用;若其他申请使用该资源,那么申请进程必须等到该资源被释放为止;
占有并等待:一个进程必须占有至少一个资源,并等待另一个资源,而该资源为其他进程所占有;
非抢占:进程不能被抢占,即资源只能被进程在完成任务后自愿释放
循环等待:若干进程之间形成一种头尾相接的环形等待资源关系

3). 死锁的处理

解决死锁的基本方法主要有 预防死锁、避免死锁、检测死锁、解除死锁 等。

(1) 死锁预防

死锁预防的基本思想是 只要确保死锁发生的四个必要条件中至少有一个不成立,就能预防死锁的发生,具体方法包括:
打破互斥条件:允许进程同时访问某些资源。但是,有些资源是不能被多个进程所共享的,这是由资源本身属性所决定的,因此,这种办法通常并无实用价值。
打破占有并等待条件:可以实行资源预先分配策略(进程在运行前一次性向系统申请它所需要的全部资源,若所需全部资源得不到满足,则不分配任何资源,此进程暂不运行;只有当系统能满足当前进程所需的全部资源时,才一次性将所申请资源全部分配给该线程)或者只允许进程在没有占用资源时才可以申请资源(一个进程可申请一些资源并使用它们,但是在当前进程申请更多资源之前,它必须全部释放当前所占有的资源)。但是这种策略也存在一些缺点:在很多情况下,无法预知一个进程执行前所需的全部资源,因为进程是动态执行的,不可预知的;同时,会降低资源利用率,导致降低了进程的并发性。
打破非抢占条件:允许进程强行从占有者哪里夺取某些资源。也就是说,但一个进程占有了一部分资源,在其申请新的资源且得不到满足时,它必须释放所有占有的资源以便让其它线程使用。这种预防死锁的方式实现起来困难,会降低系统性能。
打破循环等待条件:实行资源有序分配策略。对所有资源排序编号,所有进程对资源的请求必须严格按资源序号递增的顺序提出,即只有占用了小号资源才能申请大号资源,这样就不回产生环路,预防死锁的发生。

(2) 死锁避免

死锁避免的基本思想是动态地检测资源分配状态,以确保循环等待条件不成立,从而确保系统处于安全状态。所谓安全状态是指:如果系统能按某个顺序为每个进程分配资源(不超过其最大值),那么系统状态是安全的,换句话说就是,如果存在一个安全序列,那么系统处于安全状态。资源分配图算法和银行家算法是两种经典的死锁避免的算法,其可以确保系统始终处于安全状态。其中,资源分配图算法应用场景为每种资源类型只有一个实例(申请边,分配边,需求边,不形成环才允许分配),而银行家算法应用于每种资源类型可以有多个实例的场景。

(3) 死锁解除
1. 进程终止

​ 所谓进程终止是指简单地终止一个或多个进程以打破循环等待,包括两种方式:终止所有死锁进程和一次只终止一个进程直到取消死锁循环为止;

2. 资源抢占

指从一个或多个死锁进程那里抢占一个或多个资源,此时必须考虑三个问题:
  (I). 选择一个牺牲品
  (II). 回滚:回滚到安全状态
  (III). 饥饿(在代价因素中加上回滚次数,回滚的越多则越不可能继续被作为牺牲品,避免一个进程总是被回滚)

以上是关于Java并发编程面经大全——双非上岸阿里巴巴系列的主要内容,如果未能解决你的问题,请参考以下文章

硬核!Java集合面经大全——双非上岸阿里巴巴系列

硬核!Java集合面经大全——双非上岸阿里巴巴系列

Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列

Java虚拟机(JVM)面经大全——双非上岸阿里巴巴系列

硬核!MySQL数据库面经大全——双非上岸阿里巴巴系列

硬核!MySQL数据库面经大全——双非上岸阿里巴巴系列