首页 > 技术文章 > java并发:简单面试问题集锦

studyLog-share 2016-04-12 21:30 原文

多线程

Simultaneous Multithreading,简称SMT。

 

并行

并行性(parallelism)指两个或两个以上的事件在同一时刻发生,在多道程序环境下,并行性使多个程序同一时刻可在不同CPU上同时执行。

 

并发

并发的实质是一个物理CPU(也可以多个物理CPU) 在若干道程序之间多路复用。

并发性是对有限物理资源强制行使多用户共享以提高效率,离开了单位时间其实是没有意义的。

所以谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少。

 

Java中创建线程的方式

实现 Runnable接口、继承 Thread类、使用 Callable 

 

继承Thread的好处是什么

使用继承Thread的方式,在run()方法内获取当前线程直接使用this就可以了,无须使用 Thread.currentThread() 方法

 

继承Thread的缺点是什么

(1)Java 不支持多继承,如果继承了 Thread类,则不能再继承其他类

(2)任务代码与业务代码耦合严重  

 

Thread.start()与Thread.run()有什么区别?

Thread.start()方法用于启动线程,使之进入就绪状态,当cpu分配时间到该线程时,由JVM调度执行run()方法。

详解:

调用start方法后线程并没有马上执行而是处于就绪状态,这个就绪状态是指该线程已经获取了除CPU资源外的其他资源,等待获取CPU资源后才会真正处于运行状 态。

一旦run方法执行完毕,该线程就处于终止状态。 

为什么需要run()和start()方法,可以只用run()方法来完成任务吗?

需要run()、start()这两个方法是因为JVM创建一个单独的线程不同于普通方法的调用,这项工作由线程的start方法来完成,start由本地方法实现,需要显示地被调用。

使用这两个方法的另外一个好处是任何一个对象都可以作为线程运行,只要实现了Runnable接口,这就避免了因继承Thread类而造成的Java多继承问题。

 

线程的状态

 

注:图片来自网络

 

Sleep()、wait()

Java程序中wait 和 sleep都会造成某种形式的暂停,它们可以满足不同的需要。

sleep()是一个静态方法,只对当前线程有效,一个常见的错误是调用t.sleep()(注:这里的t是一个不同于当前线程的线程)。

sleep()方法仅仅释放CPU资源或者让当前线程停止执行一段时间。

wait()方法用于线程间通信,object.wait()使当前线程处于“不可运行”状态。

调用wait()方法时,当前线程要先获取该对象的监视器锁;如果事先没有获取该对象的监视器锁,则会抛出 IllegalMonitorStateException异常。

当前线程被添加到等待队列后,另一线程可以调用notify()方法唤醒等待中的线程。

  private void produce(int i) throws InterruptedException {
    synchronized (taskQueue) {
      while (taskQueue.size() == MAX_CAPACITY) {
        System.out.println("Queue is full " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
        //挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者线程可以获取该锁,然后获取队列里面的元素
        taskQueue.wait();
      }

      Thread.sleep(1000);
      taskQueue.add(i);
      System.out.println("Produced: " + i);
      taskQueue.notifyAll();
    }
  }

 

  private void consume() throws InterruptedException {
    synchronized (taskQueue) {
      while (taskQueue.isEmpty()) {
        System.out.println("Queue is empty " + Thread.currentThread().getName() + " is waiting , size: " + taskQueue.size());
        //挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者线程可以获取该锁,将生产元素放入队列
        taskQueue.wait();
      }
      Thread.sleep(1000);
      int i = (Integer) taskQueue.remove(0);
      System.out.println("Consumed: " + i);
      taskQueue.notifyAll();
    }
  }

参考资料:

How to work with wait(), notify() and notifyAll() in Java?

Note:

当前线程调用共享变量的 wait()方法后只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,则这些锁是不会被释放的。 

 

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

wait、notify、notifyAll是Java中Object对象上的三个方法;在多线程中可以把某个对象作为事件对象,通过这个对象的wait、notify和notifyAll方法来完成线程间状态通知(也即线程间协同)。

notify和notifyAll都是唤醒调用某个对象的wait方法的线程,二者的区别是:notify会唤醒一个等待线程,由于共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的;而notifyAll会唤醒所有的等待线程。

Note:

被唤醒的线程不能马上从 wait 方法返回并继续执行,它必须在获取了共享对象的监视器锁后才可以返回。

即被唤醒的线程需要和其他线程一起竞争锁,只有该线程竞争到了共享变量的监视器锁后才可以继续执行。

 

wait和notify、notifyAll方法要在同步块中调用,跟前述问题相互补充,主要是因为Java API强制要求这样做,如果你不这么做,你的代码会抛出IllegalMonitorStateException异常;还有一个原因是为了避免wait和notify之间产生竞态条件。

wait()/notify()的使用方式如下:

经典代码片段如下:

 

注:回看前述问题中代码示例,即按照这种模式编写

 

基本上wait()/notify()与sleep()/interrupt()类似,只是前者需要获取对象锁。

 

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

处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。也可以这么说,在notify()方法调用之后和等待线程醒来之前这段时间,等待线程原来的等待状态可能会改变,这就是在循环中使用wait()方法效果更好的原因。

 

如果需要等待某个事情完成后才能继续往下执行,可以使用什么方法?

Thread 类中的 join 方法可以达到这个目的,该方法不需要任何参数,且其返回值为void。

代码片段如下:

 

假设有三个线程T1,T2,T3,怎么确保它们按顺序执行?

在多线程中有多种方法可以让线程按特定顺序执行,如:可以用线程类的join()方法在一个线程中启动另一个线程T,线程T执行完成后原线程继续执行。

为了确保三个线程的顺序,应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成。

 

Thread类中的yield方法有什么作用?

它是一个静态方法,yield方法可以暂停当前正在执行的线程,暗示线程调度器可以进行下一轮的线程调度。

yield方法只保证当前线程放弃CPU占用,不能保证使其它线程一定能占用CPU,执行yield()的线程有可能在进入到暂停状态后马上又被执行。 

 

在一个对象上,多个线程是否可以调用不同的同步实例方法?

不能,因为对象同步了实例方法,某个线程调用对象的同步实例方法时获取了对象的对象锁,只有当执行完该方法释放对象锁后才能执行其它同步方法;但是多个线程可以同时访问不同实例的某个同步实例方法。

 

怎么检测一个线程是否拥有锁?

在java.lang.Thread中有一个方法叫holdsLock(),当且仅当指定线程拥有某个具体对象的锁时返回true。

 

在静态方法上使用同步会发生什么事?

  同步静态方法会获取该类的“Class”对象,所以当一个线程进入同步的静态方法中时,线程监视器获取类本身的对象锁,其它线程不能进入这个类的任何静态同步方法。它不像实例方法,因为多个线程可以同时访问不同实例的某个同步实例方法。

 

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

如果异常没有被捕获,该线程将会停止执行。

Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。

当一个未捕获异常将造成线程中断的时候,JVM会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler,并将线程和异常作为参数传递给handler的uncaughtException()方法进行处理。 

 

如何在Java中创建Immutable对象?

Immutable对象可以在没有同步的情况下共享,降低了对某个对象进行并发访问时的同步化开销。

这个问题看起来与多线程没有什么关系, 但不变性有助于简化已经很复杂的并发程序。

Java没有@Immutable这样的注解符,要创建不可变类,要实现下面几个步骤:

  • 将所有的成员声明为私有的
  • 通过构造方法初始化所有成员
  • 对变量不提供setter方法(这样就不允许直接访问这些成员)
  • 在getter方法中不直接返回对象本身,而是克隆对象并返回对象的拷贝

 

JVM中哪个参数是用来控制线程的栈堆栈小的?

-Xss参数是用来控制线程的堆栈大小的。

 

你如何在Java中获取线程堆栈?

  对于不同的操作系统,有多种方法来获得Java的线程堆栈。当你获取线程堆栈时,JVM会把所有线程的状态存到日志文件或者输出到控制台。在Windows你可以使用Ctrl+ Break组合键来获取线程堆栈,Linux下用kill -3命令。你也可以用jstack这个工具来获取,它对线程id进行操作,你可以用jps这个工具找到id。

 

如果你提交任务时,线程池队列已满发会生什么?

事实上如果一个任务不能被调度执行,那么ThreadPoolExecutor.submit()方法将会抛出一个RejectedExecutionException异常。

 

Java线程池中submit() 和 execute()方法有什么区别?

  两个方法都可以向线程池提交任务,execute()方法的返回类型是void,它定义在Executor接口中, 而submit()方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其它线程池类(如:ThreadPoolExecutor和ScheduledThreadPoolExecutor)都有这些方法。

 

什么是线程组

  在java的多线程处理中有线程组ThreadGroup的概念,ThreadGroup是为了方便线程管理出现的。我们可以统一设定线程组的一些属性,比如设置未捕获异常的处理方法,设置统一的安全策略等,也可以通过线程组方便地获得线程的一些信息。

  每一个ThreadGroup都可以包含一组子线程和一组子线程组,在一个进程中线程组是以树的方式存在,通常情况下根线程组是system线程组,system线程组下是main线程组,默认情况下第一级应用的线程组是通过main线程组创建出来的,也就是说system线程组是所有线程最顶级的父线程组。

Thread.currentThread().getThreadGroup();//可以获得当前线程的线程组

 

线程组与线程池的区别:

线程组和线程池是两个不同的概念,他们的作用完全不同,前者是为了方便线程的管理,后者是为了管理线程的生命周期,复用线程,减少创建销毁线程的开销。

 

何为基于共享容器协同的多线程模式以及基于事件协同的多线程模式?

在一些场景中我们需要在多个线程之间对共享的数据进行处理,例如经典的生产者-消费者模式,此即基于共享容器协同的多线程模式;

若一场景中有A、B两个线程,B线程需要等到某个状态或事件发生后才能继续自己的工作,而这个状态改变或者事件产生与A线程有关,此场景即基于事件协同的多线程模式

 

什么是线程死锁

死锁是指两个或两个以上的线程在执行过程中因争夺资源而造成的互相等待的现象。

在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

死锁的产生的因素:

  • 互斥

指线程对己经获取到的资源进行排它性使用,即该资源同时只由一个线程占用。

如果此时还有其他线程请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。 

  • 持有并请求

指一个线程己经持有了至少一个资源,但又提出了新的资源请求,而新资源己被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己己经获取的资源。

  • 不可剥夺

指线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。

  • 环路等待

如何避免线程死锁

造成死锁的原因其实和申请资源的顺序有很大关系,前述死锁产生的因素中只有持有并请求和环路等待条件是可以被破坏的;因此使用资源申请的有序性原则可以避免死锁。

资源申请的有序性是:假如线程 A 和线程 B 都需要资源 1、2、3、...、n 时,对资源进行排序,线程 A 和线程 B 只有在获取了资源 n-1 时才能去获取资源 n。 

 

守护线程与用户线程

Java 中的线程分为两类,分别是 daemon 线程(守护线程〉和 user 线程(用户线程)。

在JVM启动时会调用main函数,main函数所在的钱程是一个用户线程。

在JVM内部同时还启动了好多守护线程,比如:垃圾回收线程。

 

守护线程和用户线程有什么区别?

守护线程是否结束并不影响JVM的退出;当最后一个非守护线程结束时,JVM会正常退出。

言外之意,只要有一个用户线程还没结束,正常情况下JVM就不会退出。

创建守护线程的代码片段如下:

Note:

main 线程运行结束后,JVM会自动启动一个叫作 DestroyJavaVM 的线程,该线程会等待所有用户线程结束后终止JVM进程。

推荐阅读