JAVA备忘录(五):多线程面试题

一.sleep()方法和wait()方法的区别。

sleep方法是Thread类中的静态方法,使调用线程放弃CPU执行权,进行睡眠状态,也就是从运行态转化为阻塞态。sleep()方法需要传入一个long型的参数,单位为毫秒。待休眠时间结束后,线程就从阻塞态转化为就绪态,开始争取CPU时间片;另外要提的是,sleep()方法的执行并不会释放对象锁,如果线程原本持有对象锁,在进入睡眠状态后,依然持有对象锁,其他线程不能获得该对象锁
wait()方法是Object基础类中的一个非静态方法,所有Object的子孙类对象都有该方法。wait()方法的调用使当前线程挂起,从运行态转化为阻塞态,和sleep方法不一样的是,它不会自动恢复为就绪态,只有当其他线程对该对象执行notify或者notifyAll方法被唤醒时,才会转化为就绪态。另外wait方法的调用会释放线程持有的对象锁,其他线程可以获得该对象锁。所以说,wait方法必须搭配synchronized关键字一起使用,因为synchronized可以获取对象锁。

二.为什么wait,notify,notifyAll方法要在synchronized内调用?

这三个方法是Object基础类中的方法,所有Object的子孙类都拥有这些方法。wait方法的实质就是使调用线程挂起,由运行态转化为阻塞态,并且释放对象锁;而notify和notifyAll就是唤醒等待该对象锁的所有线程或者它们的其中一个,并且把对象锁让给它们。所以说,这三个的方法执行的前提,都是要持有对象锁。
而每个对象都有且仅有一个独占锁,线程只有获取该独占锁才能访问synchronized关键字修饰的方法或者代码块,所以进入synchronized代码块的线程都是持有对象锁的,满足这三个方法的调用前提,所以要在synchronized内调用。
而如果不在synchronized内调用,程序也可以通过编译,但是运行的时候会报错。

三.start()方法和run()方法的区别是什么?

这两个都是Thread类内的非静态方法。
在创建线程时,如果选择实现Runnable接口或者继承Thread类,都需要重写run方法,run方法内就是线程要执行的任务。

  • start方法的调用是同步的,也就是调用该方法会创建新的线程,由线程执行其中的run方法。并且start方法的调用时不可重复的,否则会抛出线程状态不正确的异常。
    run方法的调用是异步的,调用该方法不会创建新的线程,而是相当于一个普通的函数,可以比重复调用。因此启动一个线程的正确方法应该是用start方法。
  • start方法的调用使线程从新建态转化为就绪态,但并不保证线程会马上执行,因为就绪态的线程还需要等待CPU调度获得时间片后才能够执行其中的run方法执行任务。而新建态向就绪态的转化是单向的,不可逆的,所以这就是为什么重复调用start方法会抛出线程状态错误的异常。
    而run方法的调用,如果不是单独调用的话,会让线程从就绪态转化为运行态。

四.如何优雅地终止一个线程?

终止一个线程的方法有三种:

  • 使用Thread类内的stop方法。这个方法能使当前线程强制终止,包括finally语句块中的任务都不会执行,那么这样会出现一系列问题:比如说文件未关闭或者数据库未关闭。所以stop方法已经不建议使用了。
  • 使用volatile标识符。我们在线程内部可以自定义一个用volatile修饰的标识,在线程每次执行(比如说在一个循环内)都要判断该标识符的值是否被修改,如果被修改,则要作好线程终止的准备,比如说关闭资源,然后再终止线程。但这样的方法只能终止一个正在执行的线程,如果线程处于阻塞状态的话,是无法进行标识符的判断的。
  • 使用interrupt方法。interrupt方法也能使线程终止,但和stop方法不一样的是,它并不是暴力地让线程终止,而也是通过修改了一个中断信号进行线程间的通信;而目标线程可以通过Thread.interrupted方法来判断自己是否被调用interrupt,同样的,在适合的地方对该方法返回值进行判断,如果返回值为true,则说明需要终止,做好一系列后备工作后线程并终止。而和我们自定义标识符不一样的是,如果对一个正在阻塞状态的线程调用interrupt方法,线程就会提前退出阻塞状态,并抛出InterruptedException异常,我们就可以在catch语句中进行后备处理。

所以说最正确的方法应该是使用interrupt函数。

四.volatile关键字的作用?

volatile是java中的一个关键字,用来修饰变量,它提供了比synchronized关键字更轻量的一种同步机制。

在讨论volatile关键字之前,首先要聊一下线程在JVM中的内存模型:一个线程执行的之前,会创建一个只有自己能访问,其他线程无法访问的工作内存,并从主内存从把变量拷贝一份到工作内存之中,每次执行任务需要访问变量的时候,都访问自己工作内存中的变量,而不会访问主内存中的变量。那么这样就会造成线程通信同步问题。如果有其他变量对主内存中的变量进行了修改,而当前线程每次只访问自己工作内存的变量,所以主内存中变量的修改对其来说是不可见的。而使用volatile就能避免这一问题,volatile修饰的变量不会被线程复制到工作内存中,而是每次需要进行访问的时候都去主内存中进行读取。这样就保证变量的修改对每个线程来说都是可见的,而这也正是volatile的作用,就是保证变量的可见性。

但volatile并不能保证原子性,因为volatile是不加锁的轻量级操作,所以无法保证线程安全和操作的原子性。

五.什么是原子性、可见性、有序性?

  • 原子性:一个操作要么不被中锻,全部执行成功,要么全部执行失败。就叫在这个操作是原子性的。如果一个操作是原子性的,那么在多线程并发的环境下就不会出现变量被错误修改的问题。而非原子性的操作都会导致线程安全问题,需要我们使用同步操作(比如synchronized关键字)来将其变成原子操作。所以原子性是确保线程安全的因素之一
  • 可见性:可见性就是指各个线程对变量的修改,对每个线程来说都是可见的。从语法角度来说,就是被volatile关键字修饰的变量具有可见性。其实现原理为:一个线程执行的之前,会创建一个只有自己能访问,其他线程无法访问的工作内存,并从主内存从把变量拷贝一份到工作内存之中,每次执行任务需要访问变量的时候,都访问自己工作内存中的变量,而不会访问主内存中的变量。那么这样就会造成线程通信同步问题。如果有其他变量对主内存中的变量进行了修改,而当前线程每次只访问自己工作内存的变量,所以主内存中变量的修改对其来说是不可见的。而使用volatile就能避免这一问题,volatile修饰的变量不会被线程复制到工作内存中,而是每次需要进行访问的时候都去主内存中进行读取。这样就保证变量的修改对每个线程来说都是可见的
  • 有序性:在满足happened-before原则的前提下,为了提高执行效率,编译器和处理器会对指令进行重排序。Java提供了两个关键字volatile和synchronized来保证多线程之间操作的有序性,volatile关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现

六.什么是守护线程?

守护线程是一种特殊的线程,是服务于其他线程的线程;一个线程如果被设置为守护线程,那么当主线程终止的时候,无论守护线程是否执行完毕,它都会被终止。而如果是非守护线程,主线程的终止与其无关,会继续执行完线程内的任务。JAVA的垃圾回收线程就是典型的守护线程。

在JAVA中可以对对象调用setDaemon方法来设置其是否作为守护线程,但要在调用start方法之前执行调用,否则会抛出线程状态不正确的异常;也不应该在守护线程中使用文件流,数据库连接等操作,因为守护线程随时会被终止(包括finally的方法也不执行),因此资源很可能无法得到正确地关闭。

七.如果一个线程运行时发生异常了怎么办?

如果一个线程运行时发生了异常,如果使用了try-catch进行异常捕获的话,那么就会在catch语句内进行逻辑处理然后继续执行。如果没有进行try-catch处理的话,线程会直接被中断。

但也可以实现Thread.UncaughtExceptionHandler接口,实现uncaughtException方法来进行未捕获的异常处理。在该方法内部进行线程中断的准备工作比如说资源的关闭。具体使用方法就是对该线程调用setDefaultUncaughtExceptionHandler方法,传入一个UncaughtExceptionHandler实现类对象。

八.什么是重入锁?

重入锁是指 一个线程在获得锁之后,还可以重复获得该锁而不会发生阻塞的现象。一个重入锁关联了一个线程+一个计数器。当计数器为0时,说明该锁没有被任何线程持有。而当锁被某一线程获得时,JVM会记下该线程,并将该计数器+1,同一个线程重复这个操作不会发生阻塞,而会使计数不断增加,而每释放一次锁,计数器数字就会被-1.在这个过程中,其他线程是无法获得该锁的,直到计数器归0,才说明锁没有被任何线程持有。

ReentrantLock和Synchronized都是重入锁,区别是ReetrantLock需要手动地释放锁,否则会发生死锁。

九.什么是CAS?

CAS就是Compare-And-Swap,比较并交换。CAS是在jdk1.5之后出现的并发机制。在此之前,java都使用synchronized关键字来保证同步。但是锁操作也会导致一系列问题:

  • 加锁解锁操作会导致比较多的上下文切换和开销,引起性能问题。
  • 加锁操作会引起其他线程陷入阻塞。
  • 有可能会出现优先级倒置的情况,比如高优先级的线程在等待低优先级的线程释放锁。

那么在jdk1.5引入java.util.concurrent并发包后,有多处都使用到了CAS。相比于synchronized,CAS最大的特点就是它是一个乐观锁,也就是每个线程在执行的时候不用获得锁,在任务执行结束时再去判断自己的执行是否有效,若有效则提交执行,否则该次执行无效。

而CAS的具体实现方式是,CAS有三个操作数,分别是内存值V,旧的预期值A,要修改的新值B。当且仅当内存值与旧的预期值相等时,才把内存值修改为新值B。也就是说若内存值与预期值不一致,则说明此过程中有其他线程对内存值进行了修改,那么该次放弃该次执行。

CAS虽然提供了很高效的同步机制,但依然存在三大问题:

  • ABA问题。刚才说了,如果内存值和预期值相等的话,则执行修改。但问题是两者相等,说明期间肯定没有其他线程修改吗?肯定是不对的。因为变量有可能被修改了多次最终才和原始值相等,这就是ABA问题。那么解决这个问题的思路就是再给该变量加上一个版本号,变量的变化有可能是会逆向的,但版本号的变化理论上来说不可逆。
  • 循环时间长开销大。因为每个线程无需获得锁便开始了操作,如果在并发冲突比较严重的场景下,那么导致大量的操作失效,也就是事务回滚。那么就会增加CPU的开销。在这种情景下老老实实使用悲观锁是更好的选择。
  • 只能保证一个共享变量的原子操作。如果涉及多个变量,循环CAS无法保证操作的原子性,这个时候可以使用锁,或者使用AtomicReference类,把多个变量放在一个类中,实现多个变量的原子操作·。

十.如何判断一个线程是否拥有锁?

使用Thread内的holdsLock()方法,传入一个Object对象,判断当前线程是否持有该Object对象的对象锁,如果是,返回true,否则返回false。

十一.线程之间如何传递数据。

一般有两个方法:

  • 通过构造函数来传递。往构造函数中传入参数来实现传递数据。
  • 通过线程内方法比如说set()方法可以传递数据,或者直接访问线程内的共有变量。

十二.唤醒线程的方式?

唤醒线程的方法有几种:

  • suspend和resume:因为调用suspend方法导致的线程挂起可以使用resume方法来唤醒。但在这个过程中,线程并不会释放锁,所以其他线程也会因为无法获得该锁而陷入阻塞态,直至使用resume唤醒。但是在这个过程中由于其不会释放锁,也不像sleep方法一样休眠结束后会继续执行,所以很可能会出现死锁。而且被挂起的过程中线程的状态居然还是正在运行态。所以一系列的坑爹设定使得该方法被废弃。
  • sleep和interrupt:因为sleep方法而挂起的线程在等待休眠时间结束后便会自动被唤醒转化为就绪态。但是对其使用interrupt方法可以时期抛出一个IntereuptedException异常,可以提前唤醒该线程。
  • wait和notify:这两个方法是Object基础类中的方法。因为wait方法而被挂起的线程可以被notify方法或者notifyAll方法唤醒而变成就绪态。这个过程中线程会释放对象锁。所以这两个方法也必须配合synchronized来使用。
  • await和signal:这两个方法由Condition类提供,而Condition类由ReenTranLock对象提供。它能够明确地指定要唤醒的线程,而不是像wait和object一样要么随机唤醒线程要么唤醒全部线程。

十三.JAVA中用到的线程调度算法是什么?

抢占式。一个线程用完CPU时间片后,操作系统会根据线程优先级以及线程饥饿情况等因素计算出总的优先级,然后再进行CPU资源的分配。

十四.Thread.sleep(0)有什么作用?

Thread.sleep(0)并不是毫不作用的,它是让当前线程放弃剩下的CPU时间片。

因为JAVA中的线程调度算法是抢占式,而不是先来先服务。所以一些优先级较低的线程可能长时间饥饿。所以可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。

十五.Runnable和Thread用哪个好?

Thread是类,而Runnable是一个接口。

由于JAVA中单继承的特点,如果一个类继承了Thread类,那么它就不可以再继承其他类了,而实现Runnable接口之后可以继承其他类和实现其他接口,很明显灵活度更好。所以使用Runnale更好。

十六.Java中notify和notifyAll有什么区别?

相同点:

  • 这两个方法都是Object基础类中的非静态方法。所有Object的子孙类都有这两个方法。
  • 这两个方法都是涉及对象锁的操作,调用的前提都是必须要先持有对象锁,所以要配合synchronized关键字使用

不同点:

  • notify方法只会唤醒等待该对象锁的所有线程中的随机一个,而notifyAll会唤醒所有这些线程

十七.为什么wait/notify/notifyAll这些方法不在thread类里面?

因为JAVA中的锁都是对象级而不是线程级的。所以需要把wait/notify/notifyAll这些方法定义在Object基础类中而不是Thread类中。

那假设这三个方法定义在Thread类中会有什么问题呢?

首先是wait函数,它依然能使当前线程阻塞,但问题是它如何被其他线程唤醒呢?因为按照这个做法,唤醒线程需要知道具体要唤醒哪个线程并获得其线程锁。可以通过共享变量来实现,但是会有安全问题。

所以要放在Object类中。

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

因为一般来说,等待着同一个对象锁的线程一般可能会有很多个。而此时调用notify方法是随机唤起一个等待的线程,如果使用if来检查条件,而且又唤起了一个不满足条件的线程,该线程会直接执行下面的任务。这明显和我们的本意相悖。

所以普遍的做法是使用while循环来检查等待条件,然后直接调用notifyAll来唤醒全部线程,这样唤醒的线程还在while循环之中,还需要进行一次检查条件才能决定是否跳出循环执行下面的任务。

十九.什么是阻塞式方法?

JAVA中的阻塞式方法是指程序在调用该方法的时候,必须要等待某些事件的完成,比如说传入数据或者抛出异常或者线程执行完毕。否则程序将一直停留在该语句上,不会执行下面的语句。

典型的阻塞式方法就是Scanner方法的各个next()方法,InputStream的read()方法以及Future类的get()方法。

二十.在JAVA中Executor 和 Executors 的区别?

Executor是接口,是所有线程池的父接口,其中只声明一个execute方法用来执行线程任务。

Executors是一个工厂类,封装好了几个经典的线程池供给我们使用,免去了直接创建ThreadPoolExecutor且需要传入一系列参数的麻烦。

二十一.什么是原子操作?

原子操作就是指一个或者一系列操作要么不被中断,全部执行成功,要么全部执行失败。原子操作是避免在多线程环境下数据不一致,保证同步机制正确的手段。

在JAVA中通过锁和CAS方式(Compare And Set JDK1.5之后引入的一种基于乐观锁的同步机制)来实现原子操作。

举个例子,比如说一个int类型变量自增 a++;这个指令并不是一个原子操作,因为这个操作涉及两个分操作,一个是读取a的值,一个是将该值+1.这样的非原子操作就会引发数据不一致的问题。

在JDK1.5引入JUC并发包之中,其中的atomic包提供了int 和 long 类 型 的 原 子 包 装 类(其 基 本 的 特 性 就 是 在 多 线 程 环 境 下 , 当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时 , 不会 被 其 他 线 程 打 断 , 而 别 的 线 程 就 像 自 旋 锁 一 样 , 一 直 等 到 该 方 法 执 行 完 成 , 才 由 JVM 从 等 待 队 列 中 选 择 一 个 另 一 个 线 程 进 入) , 它 们 可 以 自 动 的 保 证 对 于 他 们 的 操 作 是 原 子 的 并 且 不 需 要 使 用 同 步 。

二十二.什么是Executors?

Executors是线程池相关的一个工厂类,其中封装好了创建ThreadPoolExecutor对象的一系列工厂方法。我们就可以直接调用其中的静态工厂方法就可以获取到一些经典的线程池对象,而无需我们手动地new一个ThreadPoolExecutor对象,同时还需要传入任务队列类型,拒绝策略等一系列复杂的参数。Executors就提供四种比较经典的线程池对象的工厂方法。

但阿里巴巴JAVA开发标准中说过不要使用Executors来创建默认的线程池,而是要手动地new一个ThreadPoolExecutor对象。

二十三.什么是 Callable 和 Future?

Callable接口类似于Runnable接口,也是创建线程使实现的一个接口。实现该接口需要重写其中的call方法。而call方法内就是线程需要执行的任务。另外相比于实现Runnable接口,Callable的功能更强大,因为其call方法可以返回值。当把该任务提交到线程池调用submit方法时,会返回一个Future类对象。

Future接口表示异步任务,其最经典的方法就是其中的get方法能够获取线程执行的返回值,也就是call方法的返回值,但该方法是一个阻塞方法。

二十四.什么是不可变对象?

不可变对象就是指 对象一旦被创建,其属性值,状态等就不能再被改变,反之就是可变对象。

像String,基础类型的包装类(Integer等),BigDecimal等都是不可变对象。

不可变对象因为其所有域都是final的,不可被修改,所以自然也不会出现线程安全的问题,因为线程安全问题的本质就是数据不一致嘛~

二十四.Interrupt方法和interrupted方法?

这两个方法都是Thread类之中的静态方法。

interrupt方法用于中断线程,但其与其他中断方法不一样,它并不是暴力地使线程中断,而是通过修改线程内的一个中断信号来完成,本质上是线程通信。

而在目标线程内部调用interrupted方法可以查看线程的中断状态,并清除原状态。如果该方法返回值是true,说明中断信号已经发出,线程内部可以进行一系列的善后处理比如说资源的关闭等操作之后,就可以提前中断线程。

所以用这个方法来中断线程比较安全,不暴力,但不保证线程会马上被中断。

二十五.yield方法?

yield方法是Thread类中的一个静态方法。是一个线程让步的操作,指把CPU资源优先让给其他线程来使用。但是在线程内调用该函数并不保证当前线程一定会挂起,然后让其他线程执行。因为其本质是让当前线程从运行态转化为就绪态,重新进行参与CPU调度,但是可能出现的情况就是它在转入就绪态后马上再次获得了CPU时间片,所以使用yield方法只是一种让步操作,并不能保证线程暂停执行任务。

二十六.synchronized是啥?

synchronized是java中的一个关键字,是一种同步机制,聊synchronized要从几个角度来说

  • 语法角度。synchronized可以作用在方法上,使方法成为一个同步方法,也所以单独地控制一个代码块,使该代码块成为同步代码块。
  • 功能角度。被synchronized修饰的方法和代码块是线程安全的,意味着每次只能有一个线程进入对应的同步代码块,其他想要进入该区域的线程只能阻塞等待,等到该线程离开同步区域,其他线程才能竞争进入该区域。所以synchronized可以实现线程安全。
  • 锁机制角度。synchronzied是基于锁实现的,并且是重入锁和悲观锁,重入锁说明该锁可以被同一个线程重复上锁操作而不会发生阻塞,当然也需要相同次数的解锁操作,才能被其他线程持有。悲观锁说明线程在进入同步区域之前必须要获得该锁,若该锁被占用,只能等待该锁的释放。 而synchronized的锁可以是对象锁也可以是类锁,如果在修饰了一个非静态方法,则该锁是一个对象锁,若修饰了一个静态方法,说明该锁是一个类锁。在声明同步代码块时,如果括号内写的是某个对象实例,那么就是对象锁,如果写的是某个类,那么就是类锁。

二十七.为什么synchronized不能锁住一个不可变对象?

不可变对象在内存中是不可被修改的,也就是说,对其引用的修改实际上并不是在原有内存地址上进行修改,而是在内存上分配一个新的对象内存地址,然后将引用指向该新地址。
所以,synchronized锁住一个不可变对象,如果该对象的引用进行了修改,那么锁住的就不是同一个对象。自然也无法实现互斥的功能。

二十八.什么是活锁?

活锁就是指:在发生资源竞争的时候,线程并不会像死锁机制那样陷入阻塞状态,而是保持着运行状态持续运行。但是因为无法申请到全部需要的资源,线程无法完成任务的执行,而是陷入了一个不断获取资源,继而释放资源的一个类似于自旋的过程。
解决活锁的方法很简单,在线程每次自旋之前设定一个随机重试时间。这样就可以降低线程之间资源申请冲突的概率,也就是错开同时申请同一个资源的时间点。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值