Java多线程总结(上部分)面试题解答

    自去年列下计划到现在一年有余,看了好几本书,以为已经过了很长时间,现在回头发现仅仅才过了一年而已,已经积累了不少知识,看来有目的的去学习还是有一些成效的。正好今天看到一个关于多线程的面试题,所以来总结过去所学,也是对过去知识的一个考验。

 

    1) 什么是线程?
   2) 线程和进程有什么区别?
    和第一条合并回答。线程和进程一句话总结就是,线程是进程的运行单位,线程共享内存空间。
继续详细的说开了去,说起线程和进程,我们可以从两个例子来说明。
    什么是进程?我们在使用电脑的时候,可以一边听音乐,一边写代码,这个就是一个多进程的事情,否则一台电脑一次只能运行一个任务,岂不是失去了很多乐趣。这个是通过cpu的分时运行来实现的,cpu先运行一段程序,然后快速的切换到另外一个程序,因为实在是太快速了,所以人类感觉不到他的切换,看起来也就是在同时运行了。当多核心cpu出现以后,线程大行其道。因为如果一个cpu是通过时间分片来运行一个个进程,那么在运行这个进程任务的时候,可以划分成很多小任务,交给各个cpu核心去并行运行,这样程序运行就更快了。
    另一个例子,Servlet和CGI。在web程序中,php使用的是fastcgi就是用的多线程模型,每一个用户请求过来,fork一个php-fpm进程与之对应。当然这个进程是可复用的。而Java的Servlet则使用的是多线程的模式,每一个用户请求对应一个线程。所以如果你应用代码使用了ThreadLocal变量,而忘记了清除,可能会出现用户信息混乱的严重线上问题。
     并发和并行。我们的程序都是并发运行的,但是是不是真的并行呢?如果我们只有一个cpu核心,看起来都是并发,但是说不上是并行。只有多核心cpu,多个线程才正在的是同时在运行。
    并发编程模型。这里不得不提两种常用编程模型。一种是并发工作者模式。这个模式下,每一个工作者线程将完成整个作业(全栈工程师)。例如生产汽车,一个工作者需要完整的生产一辆汽车出来。这种模式的正是我们现在使用最多的编程模式,他的好处是程序容易理解,缺点是多个线程共享数据导致的竞态,才有了现在多线程的复杂性(高效所付出的代价)。如果竞争资源太严重,例如依赖的一个共享变量,甚至可能会退化至单线程性能。另外一个方面是他的执行顺序没办法保证,我们知道一个线程得到了cpu资源,哪个线程先哪个后是没办法保证的(或使用效率稍低的公平锁)。 与之对应的就是流水线模式。这个是指每一个工作者只完成作业中的一部分,然后交给另外一个工作者完成剩下的部分。这个好处就是不同工种的工作者之间没有资源竞争的问题,那么也就可以拥有状态了。这种有时候要高效的多。因为遇到IO操作的时候,工作者可以马上交出cpu时间片,等到IO完成后又可以直接交给下一个工作者进行,因此在IO密集型任务的时候,要优于上一种模式。他的坏处是,优于工作者将的相互交叉,跟踪变得困难。相应的程序也因为各种callback导致阅读困难。
    具体的文章可以参考 https://huster.top/htmls/557.html
3) 如何在Java中实现线程?
4) 用Runnable还是Thread?
6) Thread 类中的start() 和 run() 方法有什么区别?
7) Java中Runnable和Callable有什么不同?
    以上几个问题合并回答。
    实现线程的方法需要了解的几个对象。要实现线程,我们不得不了解以下这几个对象了。
    Thread一切线程的起点,重点关注他的start方法,要新建一个线程就必须要new Thread对象,然后调用他的start方法来启动线程。
    Runable线程的任务,可以传给Thread的构造方法来让线程执行你的任务。所以启动多线程你得实现Runable的接口,实现他的run方法,在run方法里实现自己的业务逻辑。
    又由于Thread其实是实现了Runable接口的,所以实现线程的另外一个方法就是extends Thread并且覆写自己的run方法,然后再调用Thread对象的start方法。
    Callable接口,具有返回结果的多线程任务。他只有一个方法 V call(),你的返回结果就是V.在这个call里写你自己的业务逻辑。
    Future和Callable配合的,返回结果类。这个接口可以知道当前的任务状态,可以终止,也可以get(阻塞的)获取返回结果。
    刚开始我在想这个call到底我们该如何用。跟踪发现,他被FutureTask使用。扩展下,FutureTask实现了RunableFuture,而这个接口则具有Runable和Funture的特性。所以这个call方法和Runable里的run方法一样,是交给java的开发者去实现的,而具体调用则是jdk内部自己使用的。他的具体实现就在FuntureTask里。
    那么这个callable如何使用呢,这就得请出我们的强大的Executor了。
    Exexutor接口,只有一个方法 void executor(Runable task),可以看到他是一个无返回值的。
    ExecutorService是一个我们非常常用的接口,这个需要重点了解下。 他有几个比较重要的方法。
      1. 提交任务
            第一种,我们的callable的使用方法
            <T> Future<T> submit(Callable<T> task
     后面两种就用的比较少,所以不怎么重要了。
     <T> Future<T> submit(Runnable task, T result)
     Future<?> submit(Runnable task)
     这里的疑惑点是这个Ruable的task怎么还有返回结果?其实不然,第一个方法的Future的返回结果其实是你传入的T result,后面一个是null,所以其实是没有返回结果的,这个Future的用途仅仅是用来判断任务的运行状态的,是否完成了。
  1. shutdown
  他一共有两个方法, shutdown 和 shutdownNow。共同点是都不会再接受新提交的任务,不同点是,now会终止在队列中还在waiting状态的任务,而前者则会等待他们执行完成。并且对于正在运行的线程,shutdownNow还会调用这些线程的terminated方法来试图终止这些线程。至于能否正在的终止掉,我们知道调用线程的terminated方法只会将线程的isTerminated标志位置为true,至于运行逻辑是啥,则由线程开发者来具体决定。如果线程在sleep状态,还会抛出异常。
  1. terminated
有两个方法,一个是isTerminated一个是awaitTerminated,后面这有超时时间设置。这两个方法只有在调用了shutdown后才管用,是用来判断所有任务是否已经执行完成了的。
  1.  invokedAny和invokedAll方法
这两个方法比较有趣,他可以让你提交一批任务,前者是看谁先返回,后者则是等待所有的任务都执行完毕。
    既然说到了ExecutorService那么他的两个子类就不得不说了。
    ScheduledExecutorService: 可以运行周期和定时任务
    ThreadPoolExecutor:这个是线程池相关的。那么为什么有线程池就不需要多说了,一切都是为了节省开销和资源,道理和连接池一样,都是一种资源的复用。
    既然说到了以上两个重要的类,那么就得提一下工程方法Executors类,这个类可以产生周期任务和线程池的对象,当然你也可以手动自己去new,但是总归没有这个方便。
我觉得掌握以上这些就基本上差不多了。
8) Java中CyclicBarrier 和 CountDownLatch有什么不同?
     这两个都是用来线程同步用的。他们都是使用的一个计数器,当达到了计数器的数量以后,再接着干某个事情。那为什么还要有两个同样功能的类存在呢?他们其实也有一些区别的。我们可以举例子来说明。CountDownLathch相当于是一个汽车,走着走着停下来了(阻塞),接着来了n个人,每个人上来做一件事情,比如有的加油,有点洗车,有的修轮胎,有的修发动机。等待这些人的工作全部做完了,这个时候汽车又能继续工作了(计数结束,自动唤醒)。而CyclicBarrier呢,则是有n个人要开会,没到期就不能开。那么人一个个的进入会议室,先到的只能等待(wait),等到人一到期,这些人就接着开会(全部被唤醒)。另外一个点是CountDownLatch则是不能重用的,而CyclicBarrier是可以重用的。
9) Java内存模型是什么?
    这个就比较复杂了,参考总结 https://huster.top/htmls/637.html
10) Java中的volatile 变量是什么?
    了解了JMM之后就能理解volati是什么了,他能够禁止重排序,以及他的读写都是从主存里同步的,以此来保证变量的可见性。同时他因为禁止重排序,可以防止long和double的原子性。但是他的++自增操作不是原子性的,所以不是线程安全的。
11) 什么是线程安全?Vector是一个线程安全类吗?
所谓的线程安全,就是程序的多线程计算结果是可以预料并且确定的,和单线程的计算结果是一样的。这样的多线程程序,我们就说他是线程安全的。vector和arrayList,LinkedList一样是,数组的一种实现,通过同步方法来保证线程的安全。
12) Java中什么是竞态条件? 举个例子说明。
    概念性问题。竞态就是在多线程环境下,对于一个共享资源产生的一种竞争状态,如果使用不当,可能会导致多线程的问题。比如多线程下,一个共享变量的自增操作。a++,由于在自增的时候,需要先读取当前的变量的值,加1以后,再写回去。那么对于这个变量的读和写,就会产生竞态。

13) Java中如何停止一个线程?

    严格的来讲,Java无法强制性的结束一个线程。但是每个线程都有一个标志位,表示的是当前线程是否被中断。你可以调用线程的中断的方法,将这个标志位设置成true,然后业务方在处理的时候,读取这个标志位,如果发现标志位为中断状态,则退出当前线程的任务。也就是说系统没有直接提供给你中断的方法,但是根据api,多线程程序员可以完成。

14) 一个线程运行时发生异常会怎样?

线程的可检出异常必须处理,否则编译都无法通过,这个是有现成开发者自己去解决的问题。如果是运行时unchecked异常呢? 多线程的异常在多线程内部,你的主线程是try catch不到的。那么如果解决这个问题呢? 你需要首先定义一个Exception handler,然后调用现成的setUncaughtExceptionHandler让你的handleer来处理多线程的异常。如果觉得太麻烦,你还可以设置一个默认的,setDefaultUncaughtExceptionHandler. 特别注意,定时任务在发生异常的时候,如果不处理,会停止周期运行的任务,并且不会有任何提示,让你一脸懵逼。

15) 如何在两个线程间共享数据?

我觉得有两种方法。一种是共享对象,另外一种方法是通过阻塞队列组成一个生产者、消费者模型来传递数据。

16) Java中notify 和 notifyAll有什么区别?

没有太明白这个题目的真正意义,notify会在一批的wait程序里,唤醒一种一个继续执行。至于他能否成功获得锁继续自己的任务,另当别论。而notifyAll则是唤醒所有的wait线程,这些线程有可能会再次竞争锁,可能只有一个线程真正拿到锁去执行。

17) 为什么wait, notify 和 notifyAll这些方法不在thread类里面?

因为wait、notify、notifyALL都伴随着锁的竞争,而锁的定义是锚点在一个对象上的。如果设置在一个Thread内部,如何实现线程之间的锁的竞争呢

18) 什么是ThreadLocal变量?

ThreadLocal是线程内部的一个变量,每一个线程拥有自己的一个私域空间。每个线程先进去的和读出来的变量都仅仅是对自己可见的。他的实现方式是一个map,map的key就是当前的线程,value就是你存进去的值。因此每个线程才能有自己的一个副本。另外,需要注意的是tomcat写程序的时候,由于线程间是共享的(多个不同的用户请求进来),所以用户信息等需要在线程结束的时候清除,否则会出现信息错乱的问题。

19) 什么是FutureTask?

futureTask就是可以获取执行结果,可以取消,可以查询状态等的一个任务。当调用get方法的时候,会阻塞一直到多线程执行完毕,有结果返回为止。实际上这个类我们开发者一般用不到,是JDK里面调用Callable的call方法的地方。是JDK里面实现Callable任务需要用的。

20) Java中interrupted 和 isInterrupted方法的区别?

注意区分这几个方法, Interrupt是一个动词是将当前的中断标志位设置为true,至于会不会终止当前的线程由开发者自己来决定。而Interrupted则是测试当前的标志位,并且会清除标志位的状态,也就是会置为false。而IsInterrupted则不会清除标志位。

21) 为什么wait和notify方法要在同步块中调用?

不在同步代码块里调用就会抛异常,IllegalMonitorStateException。 更深层次的原因是,wait是要释放锁的,如果不在同步代码块里,你没有锁,却去释放锁,这不就出错了吗?同理,notify是需要抢到锁的。

22) 为什么你应该在循环中检查等待条件?

这个纯粹是为了防止假唤醒的问题。有时候wait中的线程会在没有接到notify的情况下醒来(假唤醒), 没有while的情况下,他就不会检查标志位,而继续去执行自己该执行的任务,这可能会导致严重的问题。

23) Java中的同步集合与并发集合有什么区别?

首先什么是同步集合。Collections提供了很多同步的集合,例如Collections.synchronizeList, Collections.synchronizeMap等。并发集合是指concurrentMap等。他们区别是同步集合是通过在集合上添加锁来实现的,性能不如并发集合。特别是concurrentMap使用了分段锁的实现方式。在Java8之后则元素少的时候还是使用的链表,但是在元素多的时候使用的红黑树,更加高效。

24) Java中堆和栈有什么不同?

            参考上一篇文章 https://huster.top/htmls/637.html

25) 什么是线程池? 为什么要使用它?

     我们首先要知道创建一个线程是有代价的。从内存模型中,我们就知道每一个线程都有自己的栈空间,因此至少在内存使用上,每一个线程都会耗费资源。更何况线程也是一个对象,一个线程的创建和销毁都是需要耗费资源的。因此,如果线程可以复用,不就可以节省很多资源吗? 线程池正是这个作用的。
他涉及到以下几个对象:首先是接口Executor, 他有一个方法 execute(Runnable task) 可以用来给你执行Runnable的任务
另外一个是 ExecutorService接口,这个接口就更厉害了。他可以实现这么几个功能。运行一批任务,如果有一个完成,就能拿到结果(InvokeAny)。 运行一批任务,如果全部完成,在拿到结果(InvokeAll)。他还能submit接受一个callable的任务,这样你就可以拿到这个线程的执行结果了。线程池用完了以后,需要shutdown,否则他会永远不会结束。比如你写在了main方法里,如果你不shutdown,那么这个方法永远不会结束。还有对应的shutdownNow的方法。注意shutdown的方法是说从现在开始我不接受新任务了,原来的还在运行的任务会等待他的结束。shutdownNow则会立即停止(发送interrupts信号,能不能停住看业务方的实现了)。
而我们常用的线程池是ThreadPoolExecutor,他就是你使用工厂方法Executors.newCachedThreadPool所创建的对象,你也可以不用工厂方法,自己new一个,其实是一样的。实际上在工厂方法内部调用的正是他的构造方法。
与之对应的还有一个ScheduledExecutorService,他是用来运行一个周期性的任务的。他可以实现的功能是延迟多少秒之后开始执行任务和延迟多少秒之后,每隔多长时间运行一个任务。注意的是,你要小心的处理你的线程的异常,否则你的异常既不会被捕获,你的周期任务也会死掉。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注