Java面试题——多线程高并发

  1. 什么是线程?
    线程是进程中的一个实体,是被系统独立调度和分派的基本单位,它被包含在进程之中,是进程中的实际运作单位。线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程也有就绪、阻塞和运行三种基本状态。我们通过多线程编程,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。

  2. 线程和进程有什么区别?
    i):在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,也是抢占处理机的调度单位,而把线程作为独立运行和独立调度的基本单位。在多线程OS中,进程不是一个可执行的实体。
    ii):对于地址空间和其它资源(如打开文件):进程间相互独立,同一进程的各线程间共享。某进程内的线程在其它进程不可见。线程是进程的子集,一个进程可以有很多线程。
    iii):通信:不同进程之间通过IPC(进程间通信)接口进行通信。同一进程的线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。
    iv):调度和切换:线程上下文切换比进程上下文切换要快得多。

  3. Java中有哪些方式可以实现线程?哪种更好?
    在语言层面有两种方式。可以继承java.lang.Thread线程类,但是它需要调用java.lang.Runnable接口来执行。由于线程类本身就是调用的Runnable接口,所以你可以继承java.lang.Thread 类或者直接调用Runnable接口来重写run()方法实现线程。
    如果是想实现多继承,则调用Runnable接口。

  4. sleep()和wait()有什么区别?
    sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
    wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。
    see more:http://blog.csdn.net/HaixWang/article/details/79376181

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复。

wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。如果线程重新获得对象的锁就可以进入就绪状态(还是等待)
see more:http://blog.csdn.net/HaixWang/article/details/79376181

在JDK1.2以前的版本如果要实现线程的暂停、恢复和停止的方法分别是suspend()、resume()、stop()。但是从JDK1.2以后这些方法已经被遗弃

共同点:都抛出InterruptedException异常

yield方法会临时暂停当前正在执行的线程,来让有同样优先级的正在等待的线程有机会执行。**如果没有正在等待的线程,或者所有正在等待的线程的优先级都比较低,那么该线程会继续运行。**执行了yield方法的线程什么时候会继续运行由线程调度器来决定,不同的厂商可能有不同的行为。yield方法不保证当前的线程会暂停或者停止,(也就是说不保证时效),但是可以保证当前线程在调用yield方法时会放弃CPU。

  1. 为什么需要并行设计?
    业务需求:业务上需要多个逻辑单元,比如多个客户端要发送请求
    性能需求:在多核OS中,使用多线程并发执行性能会比单线程执行的性能好很多

  2. 并发和并行的区别:
    解释一:并行是指两个或者多个线程在同一时刻发生;而并发是指两个或多个线程在同一时间间隔发生(交替运行)。
    解释二:并行是在不同实体上的多个事件(多个JVM),并发是在同一实体上的多个事件(一个JVM)。
    并行又分在一台处理器上同时处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群

  3. 进程死锁的四个必要条件以及解除死锁的基本策略:
    (1)互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。
    (2)请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。
    (3)不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
    (4)环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”。
    1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件
    2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁
    3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉
    4.解除死锁:该方法与检测死锁配合使用

  4. 解释一下活锁:
    是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。释放完后,双方发现资源满足需求了,又都去强占资源,但是又只拿到一部分,就这样,资源在各个线程间一直往复。这样你让我,我让你,最后两个线程都无法使用资源。

  5. Thread 类中的start() 和 run() 方法有什么区别?
    i):首先,start方法内部会调用run方法。
    ii):start与run方法的主要区别在于当程序调用start方法一个新线程将会被创建,并且在run方法中的代码将会在新线程上运行,然而在你直接调用run方法的时候,程序并不会创建新线程,run方法内部的代码将在当前线程上运行。大多数情况下调用run方法是一个bug或者变成失误。因为调用者的初衷是调用start方法去开启一个新的线程,这个错误可以被很多静态代码覆盖工具检测出来,比如与fingbugs. 如果你想要运行需要消耗大量时间的任务,你最好使用start方法,否则在你调用run方法的时候,你的主线程将会被卡住。
    iii):还有一个区别在于,一但一个线程被启动,你不能重复调用该thread对象的start方法,调用已经启动线程的start方法将会报IllegalStateException异常, 而你却可以重复调用run方法。

  6. 概括的解释下线程的几种可用状态。
    答:
    线程在执行过程中,可以处于下面几种状态:

  • 创建(New):新创建了一个线程对象。
  • 就绪(Runnable): 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权(或者说cpu时间片)
  • 运行(Running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码
  • 阻塞(Waiting): 阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。阻塞的情况分三种:
    (一). 等待阻塞:运行(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)状态.阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。
  • 死亡(Dead):线程完成了执行,或者被强制结束执行。线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
    **补充:**可运行状态
    调用线程的start()方法,此线程进入可运行状态。
    当前线程sleep()方法结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态。
    当前线程时间片用完了,调用当前线程的yield()方法,当前线程进入可运行状态。
    锁池里的线程拿到对象锁后,进入可运行状态。
  1. CyclicBarrier 和 CountDownLatch有什么不同?
    CyclicBarrier 和 CountDownLatch 都可以用来让一组线程等待其它线程。与 CyclicBarrier 不同的是,CountdownLatch 不能重新使用。点此查看更多信息和示例代码。(http://javarevisited.blogspot.com/2012/07/cyclicbarrier-example-java-5-concurrency-tutorial.html)

  2. Java内存模型是什么?
    Java内存模型规定和指引Java程序在不同的内存架构、CPU和操作系统间有确定性地行为。它在多线程的情况下尤其重要。Java内存模型对一个线程所做的变动能被其它线程可见提供了保证,它们之间是先行发生关系。这个关系定义了一些规则让程序员在并发编程时思路更清晰。比如,先行发生关系确保了:
    线程内的代码能够按先后顺序执行,这被称为程序次序规则。
    对于同一个锁,一个解锁操作一定要发生在时间上后发生的另一个锁定操作之前,也叫做管程锁定规则。
    前一个对volatile的写操作在后一个volatile的读操作之前,也叫volatile变量规则。
    一个线程内的任何操作必需在这个线程的start()调用之后,也叫作线程启动规则。
    一个线程的所有操作都会在线程终止之前,线程终止规则。
    一个对象的终结操作必需在这个对象构造完成之后,也叫对象终结规则。
    可传递性
    volatile不具备原子性,有效阻止指令重排序。

  3. Java中的volatile 变量是什么?
    volatile是一个特殊的修饰符,只有成员变量才能使用它。在Java并发程序缺少同步类的情况下,多线程对成员变量的操作对其它线程是透明的。volatile变量可以保证下一个读取操作会在前一个写操作之后发生,就是上一题的volatile变量规则。查看更多volatile的相关内容。(http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html)

  4. Java线程池
    Java通过Executors提供四种线程池,分别为:

  • newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。
  • newFixedThreadPool 创建一个指定大小的线程池,可控制线程最大并发数,超出的线程会在队列中等待。
  • newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
  • newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
  1. 线程的共享资源和私有资源
    线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID。

线程拥有这许多共性的同时,还拥有自己的个性。有了这些个性,线程才能实现并发性。这些个性包括:

1.线程ID
  每个线程都有自己的线程ID,这个ID在本进程中是唯一的。进程用此来标识线程。

2.寄存器组的值
   由于线程间是并发运行的,每个线程有自己不同的运行线索,当从一个线程切换到另一个线程上时,必须将原有的线程的寄存器集合的状态保存,以便将来该线程在被重新切换到时能得以恢复。

3.线程的堆栈
   堆栈是保证线程独立运行所必须的。
   线程函数可以调用函数,而被调用函数中又是可以层层嵌套的,所以线程必须拥有自己的函数堆栈,使得函数调用可以正常执行,不受其他线程的影响。

4.错误返回码
   由于同一个进程中有很多个线程在同时运行,可能某个线程进行系统调用后设置了errno值,而在该线程还没有处理这个错误,另外一个线程就在此时被调度器投入运行,这样错误值就有可能被修改。所以,不同的线程应该拥有自己的错误返回码变量。

5.线程的信号屏蔽码
   由于每个线程所感兴趣的信号不同,所以线程的信号屏蔽码应该由线程自己管理。但所有的线程都共享同样的信号处理器。

6.线程的优先级
   由于线程需要像进程那样能够被调度,那么就必须要有可供调度使用的参数,这个参数就是线程的优先级。
  1. 自旋锁

互斥同步的开销都很大,并且,在大部分场景中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时**执行忙循环(自旋)**一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。

自选锁虽然通过避免进入阻塞状态能减少开销,但是它需要进行忙循环操作占用 CPU 时间,所以它只适用于共享数据的锁定状态很短的场景。

在 JDK 1.6 中引入了自适应的自旋锁(自旋的时间不再固定),由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定

  1. 怎么检测一个线程是否拥有锁
    java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁。

  2. volatile 与 synchronized 的比较
    volatile主要用在多个线程感知实例变量被更改了场合,从而使得各个线程获得最新的值。它强制线程每次从主内存中讲到变量,而不是从线程的私有内存中读取变量,从而保证了数据的可见性。
    关于synchronized,可参考:JAVA多线程之Synchronized关键字–对象锁的特点
    比较:
    ①volatile轻量级,只能修饰变量。synchronized重量级,还可修饰方法
    ②volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞。
    synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才能进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象时,会出现阻塞。

你可能感兴趣的:(多线程高并发,面试题-java)