写点什么

不会多线程还想进 BAT?精选 19 道多线程面试题,有答案边看边学

作者:程序知音
  • 2022 年 7 月 29 日
  • 本文字数:11119 字

    阅读完需:约 36 分钟


一. Java 程序如何停止一个线程?

建议使用”异常法”来终止线程的继续运行。在想要被中断执行的线程中, 调用 interrupted()方法,该方法用来检验当前线程是否已经被中断,即该线程 是否被打上了中断的标记,并不会使得线程立即停止运行,如果返回 true,则 抛出异常,停止线程的运行。在线程外,调用 interrupt()方法,使得该线程打 上中断的标记。

二. 说一下 java 中的多线程。

1. Java 中实现多线程的四种方式(创建多线程的四种方式)?

①. 继承 Thread 类创建线程类

  • 定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方 法体就代表了线程要完成的任务。因此把 run()方法称为执行体。

  • 创建 Thread 子类的实例,即创建了线程对象。

  • 调用线程对象的 start()方法来启动该线程。

②. 通过 Runnable 接口创建线程类

  • 定义 Runnable 接口的实现类,并重写该接口的 run()方法,该 run() 方法的方法体同样是该线程的线程执行体。

  • 创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。

  • 调用线程对象的 start()方法来启动该线程。

③. 通过 Callable 和 Future 创建线程

  • 创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作 为线程执行体,并且有返回值。

  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call()方法的返回值。

  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线 程。

  • 调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值。

④. 通过线程池创建线程

利用线程池不用 new 就可以创建线程,线程可复用,利用 Executors 创 建线程池。

扩展 1:Java 中 Runnable 和 Callable 有什么不同?

  • Callable 定义的方法是 call(),而 Runnable 定义的方法是 run()。

  • Callable 的 call 方法可以有返回值,而 Runnable 的 run 方法不能有 返回值。

  • Callable 的 call 方法可抛出异常,而 Runnable 的 run 方法不能抛出 异常。

扩展 2:一个类是否可以同时继承 Thread 和实现 Runnable 接口?<

可以。比如下面的程序可以通过编译。因为 Test 类从 Thread 类中继承了 run()方法,这个 run()方法可以被当作对 Runnable 接口的实现。

public class Test extends Thread implements Runnable { public static void main(String[] args) { Thread t = new Thread(new Test()); t.start(); }}
复制代码

2. 实现多线程的同步。

在多线程的环境中,经常会遇到数据的共享问题,即当多个线程需要访问同 一资源时,他们需要以某种顺序来确保该资源在某一时刻只能被一个线程使用, 否则,程序的运行结果将会是不可预料的,在这种情况下,就必须对数据进行 同步。

在 Java 中,提供了四种方式来实现同步互斥访问: synchronized 和 Lock 和 wait()/notify()/notifyAll()方法和 CAS。

①. synchronized 的用法

A . 同步代码块

synchronized 块写法: synchronized(object) {}
复制代码

表示线程在执行的时候会将 object 对象上锁。(注意这个对象可以是任意 类的对象,也可以使用 this 关键字或者是 class 对象)。

可能一个方法中只有几行代码会涉及到线程同步问题,所以 synchronized 块 比 synchronized 方法更加细粒度地控制了多个线程的访问, 只有 synchronized 块中的内容不能同时被多个线程所访问,方法中的其他语句仍然 可以同时被多个线程所访问(包括 synchronized 块之前的和之后的)。

B . 修饰非静态的方法

当 synchronized 关键字修饰一个方法的时候,该方法叫做同步方法。

Java 中的每个对象都有一个锁(lock),或者叫做监视器(monitor), 当一个线程访问某个对象的 synchronized 方法时,将该对象上锁,其他任何 线程都无法再去访问该对象的 synchronized 方法了(这里是指所有的同步方 法,而不仅仅是同一个方法),直到之前的那个线程执行方法完毕后(或者是 抛出了异常),才将该对象的锁释放掉,其他线程才有可能再去访问该对象的 synchronized 方法。

注意这时候是给对象上锁,如果是不同的对象,则各个对象之间没有限制 关系。

注意,如果一个对象有多个 synchronized 方法,某一时刻某个线程已经进入 到了某个 synchronized 方法,那么在该方法没有执行完毕前,其他线程是无法访 问该对象的任何 synchronized 方法的。

C . 修饰静态的方法

当一个 synchronized 关键字修饰的方法同时又被 static 修饰,之前说过, 非静态的同步方法会将对象上锁,但是静态方法不属于对象,而是属于类,它 会将这个方法所在的类的 Class 对象上锁。一个类不管生成多少个对象,它们 所对应的是同一个 Class 对象。

因此,当线程分别访问同一个类的两个对象的两个 static,synchronized 方法时,它们的执行顺序也是顺序的,也就是说一个线程先去执行方法,执行 完毕后另一个线程才开始。

结论:

  • synchronized 方法是一种粗粒度的并发控制,某一时刻,只能有一个线 程执行该 synchronized 方法。

  • synchronized 块则是一种细粒度的并发控制,只会将块中的代码同步, 位于方法内,synchronized 块之外的其他代码是可以被多个线程同时访问到 的。

②.Lock 的用法

使用 Lock 必须在 try-catch-finally 块中进行,并且将释放锁的操作放在 finally 块中进行,以保证锁一定被释放,防止死锁的发生。通常使用 Lock 来 进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;lock.lock();try{ //处理任务}catch(Exception ex){}finally{ lock.unlock(); //释放锁}
复制代码

Lock 和 synchronized 的区别和 Lock 的优势。你需要实现 一个高效的缓存,它允许多个用户读,但只允许一个用户写,以此 来保持它的完整性,你会怎样去实现它?

  • Lock 是一个接口,而 synchronized 是 Java 中的关键字, synchronized 是内置的语言实现;

  • synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导 致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放 锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁;

  • Lock 可以让等待锁的线程响应中断(可中断锁),而 synchronized 却不行,使用 synchronized 时,等待的线程会一直等待下去,不能够响应中 断(不可中断锁);

  • 通过 Lock 可以知道有没有成功获取锁(tryLock()方法:如果获取 了锁,则返回 true;否则返回 false,也就说这个方法无论如何都会立即返回。 在拿不到锁时不会一直在那等待。),而 synchronized 却无法办到。

  • Lock 可以提高多个线程进行读操作的效率(读写锁)。

  • Lock 可以实现公平锁,synchronized 不保证公平性。 在性能上来说,如果线程竞争资源不激烈时,两者的性能是差不多的,而 当竞争资源非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优 于 synchronized。所以说,在具体使用时要根据适当情况选择。

扩展 1: volatile 和 synchronized 区别。

  • volatile 是变量修饰符,而 synchronized 则作用于代码块或方法。

  • volatile 不会对变量加锁,不会造成线程的阻塞;synchronized 会 对变量加锁,可能会造成线程的阻塞。

  • volatile 仅能实现变量的修改可见性,并不能保证原子性;而 synchronized 则 可 以 保 证 变 量 的 修 改 可 见 性 和 原 子 性 。 (synchronized 有两个重要含义:它确保了一次只有一个线程可以执 行代码的受保护部分(互斥),而且它确保了一个线程更改的数据对于 其它线程是可见的(更改的可见性),在释放锁之前会将对变量的修改 刷新到主存中)。

  • volatile 标记的变量不会被编译器优化,禁止指令重排序; synchronized 标记的变量可以被编译器优化。

扩展 2:什么场景下可以使用 volatile 替换 synchronized?

只需要保证共享资源的可见性的时候可以使用 volatile 替代, synchronized 保证可操作的原子性,一致性和可见性。

③.wait()\notify()\notifyAll()的用法(Java 中怎样唤醒一个阻塞的线程?)

在 Java 发展史上曾经使用 suspend()、resume()方法对于线程进行阻塞唤醒,但随之出 现很多问题,比较典型的还是死锁问题。

解决方案可以使用以对象为目标的阻塞,即利用 Object 类的 wait()和 notify()方法实现 线程阻塞。

首先,wait、notify 方法是针对对象的,调用任意对象的 wait()方法都将导致线程阻塞, 阻塞的同时也将释放该对象的锁,相应地,调用任意对象的 notify()方法则将随机解除该对 象阻塞的线程,但它需要重新获取改对象的锁,直到获取成功才能往下执行;其次,wait、 notify 方法必须在 synchronized 块或方法中被调用,并且要保证同步块或方法的锁对象与调 用 wait、notify 方法的对象是同一个,如此一来在调用 wait 之前当前线程就已经成功获取 某对象的锁,执行 wait 阻塞后当前线程就将之前获取的对象锁释放。

扩展 1: 为什么 wait(),notify(),notifyAll()等方法都定义在 Object 类中?

因为这三个方法都需要定义在同步代码块或同步方法中,这些方法的调用是依赖锁对 象的,而同步代码块或同步方法中的锁对象可以是任意对象,那么能被任意对象调用的方 法一定定义在 Object 类中。

扩展 2: notify()和 notifyAll()有什么区别?

notify()和 notifyAll()都是 Object 对象用于通知处在等待该对象的线程的方法。

void notify(): 唤醒一个正在等待该对象的线程,进入就绪队列等待 CPU 的调度。

void notifyAll(): 唤醒所有正在等待该对象的线程,进入就绪队列等待 CPU 的调度。

两者的最大区别在于:

notifyAll 使所有原来在该对象上等待被 notify 的线程统统退出 wait 的状态,变成等待该对 象上的锁,一旦该对象被解锁,他们就会去竞争。 notify 他只是选择一个 wait 状态线程进行通知,并使它获得该对象上的锁,但不惊动其他 同样在等待被该对象 notify 的线程们,当第一个线程运行完毕以后释放对象上的锁,此时 如果该对象没有再次使用 notify 语句,即便该对象已经空闲,其他 wait 状态等待的线程由 于没有得到该对象的通知,继续处在 wait 状态,直到这个对象发出一个 notify 或 notifyAll, 它们等待的是被 notify 或 notifyAll,而不是锁。

④.CAS

它是一种非阻塞的同步方式。具体参见上面的部分。

扩展一:同步锁的分类?

  • Synchronized 和 Lock 都是悲观锁。

  • 乐观锁,CAS 同步原语,如原子类,非阻塞同步方式。

扩展二:锁的分类?

  • 一种是代码层次上的,如 java 中的同步锁,可重入锁,公平锁,读写锁。另外一种是数据库层次上的,比较典型的就是悲观锁和乐观锁,表锁,行锁,页锁。

扩展三:java 中的悲观锁和乐观锁?

  • 悲观锁:悲观锁是认为肯定有其他线程来争夺资源,因此不管到底会不会发生争夺, 悲观锁总是会先去锁住资源,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放 锁。Synchronized 和 Lock 都是悲观锁。

  • 乐观锁:每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直 到成功为止。就是当去做某个修改或其他操作的时候它认为不会有其他线程来做同样的操 作(竞争),这是一种乐观的态度,通常是基于 CAS 原子指令来实现的。CAS 通常不会将 线程挂起,因此有时性能会好一些。乐观锁的一种实现方式——CAS。


三. 实现线程之间的通信?

当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。

1. Object 类中 wait()\notify()\notifyAll()方法

2. 用 Condition 接口

  • Condition 是被绑定到 Lock 上的,要创建一个 Lock 的 Condition 对 象必须用 newCondition()方法。在一个 Lock 对象里面可以创建多个 Condition 对象,线程可以注册在指定的 Condition 对象中,从而可以有 选择性地进行线程通知,在线程调度上更加灵活。

  • 在 Condition 中,用 await()替换 wait(),用 signal()替换 notify(), 用 signalAll()替换 notifyAll(),传统线程的通信方式, Condition 都可以实现。 调用 Condition 对象中的方法时,需要被包含在 lock()和 unlock()之间。

3. 管道实现线程间的通信

  • 实现方式:一个线程发送数据到输出管道流,另一个线程从输入管道流中 读取数据。

  • 基本流程:

  • 1)创建管道输出流 PipedOutputStream pos 和管道输入流 PipedInputStream pis。

  • 2)将 pos 和 pis 匹配,pos.connect(pis)。

  • 3)将 pos 赋给信息输入信息的线程,pis 赋给获取信息的线程,就可以实 现线程间的通讯了。

  • 缺点:

  • 1)管道流只能在两个线程之间传递数据。

  • 线程 consumer1 和 consumer2 同时从 pis 中 read 数据,当线程 producer 往管道流中写入一段数据(1,2,3,4,5,6)后,每一个时刻只有一个 线程能获取到数据,并不是两个线程都能获取到 producer 发送来的数据,因 此一个管道流只能用于两个线程间的通讯。

  • 2)管道流只能实现单向发送,如果要两个线程之间互通讯,则需要两个管道流。

  • 线程 producer 通过管道流向线程 consumer 发送数据,如果线程 consumer 想给线程 producer 发送数据,则需要新建另一个管道流 pos1 和 pis1,将 pos1 赋给 consumer1,将 pis1 赋给 producer1。

4. 使用 volatile 关键字

见上面部分。

四. 如何确保线程安全?

如果多个线程同时运行某段代码,如果每次运行结果和单线程运行的结果 是一样的,而且其他变量的值也和预期的是一样的,就是线程安全的。

Synchronized,Lock,原子类(如 atomicinteger 等),同步容器、并 发容器、 阻塞队列 、 同步辅助类(比 如 CountDownLatch, Semaphore, CyclicBarrier)。

五. 多线程的优点和缺点?

1. 优点:

  • 充分利用 cpu,避免 cpu 空转。

  • 程序响应更快。

2. 缺点:

  • 上下文切换的开销

  • 当 CPU 从执行一个线程切换到执行另外一个线程的时候,它需要先存储当 前线程的本地的数据,程序指针等,然后载入另一个线程的本地数据,程序指 针等,最后才开始执行。这种切换称为“上下文切换”。CPU 会在一个上下文 中执行一个线程,然后切换到另外一个上下文中执行另外一个线程。上下文切 换并不廉价。如果没有必要,应该减少上下文切换的发生。

  • 增加资源消耗

  • 线程在运行的时候需要从计算机里面得到一些资源。 除了 CPU,线程还需 要一些内存来维持它本地的堆栈。它也需要占用操作系统中一些资源来管理线 程。

  • 编程更复杂

  • 在多线程访问共享数据的时候,要考虑线程安全问题。

六. 写出 3 条你遵循的多线程最佳实践。

  1. 给线程起个有意义的名字。

  2. 避免锁定和缩小同步的范围 。 相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。

  3. 多用同步辅助类,少用 wait 和 notify 。 首先,CountDownLatch, Semaphore, CyclicBarrier 这些同步辅助 类简化了编码操作,而用 wait 和 notify 很难实现对复杂控制流的控制。其次, 这些类是由最好的企业编写和维护在后续的 JDK 中它们还会不断优化和完善, 使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化。

  4. 多用并发容器,少用同步容器。 如果下一次你需要用到 map,你应该首先想到用 ConcurrentHashMap。

七. 多线程的性能一定就优于单线程吗?

不一定,要看具体的任务以及计算机的配置。比如说:

  • 对于单核 CPU,如果是 CPU 密集型任务,如解压文件,多线程的性能反 而不如单线程性能,因为解压文件需要一直占用 CPU 资源,如果采用多线程, 线程切换导致的开销反而会让性能下降。如果是交互类型的任务,肯定是需要 使用多线程的。

  • 对于多核 CPU,对于解压文件来说,多线程肯定优于单线程,因为多个线 程能够更加充分利用每个核的资源。

八. 多线程中锁的种类。

1. 可重入锁

ReentrantLock 和 synchronized 都是可重入锁。

如果当前线程已经获得了某个监视器对象所持有的锁,那么该线程在该方法 中调用另外一个同步方法也同样持有该锁。

比如:

public sychrnozied void test() { xxxxxx; test2();}public sychronized void test2() { yyyyy;}
复制代码

在上面代码段中,执行 test 方法需要获得当前对象作为监视器的对象锁, 但方法中又调用了 test2 的同步方法。

如果锁是具有可重入性的话,那么该线程在调用 test2 时并不需要再次获 得当前对象的锁,可以直接进入 test2 方法进行操作。

可重入锁最大的作用是避免死锁。如果锁是不具有可重入性的话,那么该 线程在调用 test2 前会等待当前对象锁的释放,实际上该对象锁已被当前线程 所持有,不可能再次获得,那么线程在调用同步方法、含有锁的方法时就会产 生死锁。

2. 可中断锁

顾名思义,就是可以响应中断的锁。

在 Java 中,synchronized 不是可中断锁,而 Lock 是可中断锁。 lockInterruptibly()的用法已经体现了 Lock 的可中断性。如果某一线程 A 正 在执行锁中的代码,另一线程 B 正在等待获取该锁,可能由于等待时间过长, 线程 B 不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线 程中断它,这种就是可中断锁。

3. 公平锁

在 Java 中,synchronized 就是非公平锁,它无法保证等待的线程获取锁 的顺序。而对于 ReentrantLock 和 ReentrantReadWriteLock,它默认情况 下是非公平锁,但是可以设置为公平锁。

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个 锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该锁, 这种就是公平锁。

4. 读写锁

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。 ReadWriteLock 就是读写锁,它是一个接口,ReentrantReadWriteLock 实 现了这个接口。可以通过 readLock()获取读锁,通过 writeLock()获取写锁。


九. 锁优化

1. 自旋锁

  • 为了让线程等待,让线程执行一个忙循环(自旋)。需要物理机器有一个以 上的处理器。自旋等待虽然避免了线程切换的开销,但它是要占用处理器时间 的,所以如果锁被占用的时间很短,自旋等待的效果就会非常好,反之自旋的 线程只会白白消耗处理器资源。自旋次数的默认值是 10 次,可以使用参数 -XX:PreBlockSpin 来更改。

  • 自适应自旋锁:自旋的时间不再固定,而是由前一次在同一个锁上的自旋 时间及锁的拥有者的状态来决定。

2. 锁清除

  • 指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不 可能存在共享数据竞争的锁进行清除(逃逸分析技术:在堆上的所有数据都不会 逃逸出去被其它线程访问到,可以把它们当成栈上数据对待)。

3. 锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把加锁同 步的范围扩展到整个操作序列的外部。

4. 轻量级锁

  • 在代码进入同步块时,如果此同步对象没有被锁定,虚拟机首先将在当前 线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储所对象目 前的 Mark Word 的拷贝。然后虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为执行 Lock Record 的指针。如果成功,那么这个线程就拥有了该 对象的锁。如果更新操作失败,虚拟机首先会检查对象的 Mark Word 是否指 向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,否则说 明这个对象已经被其它线程抢占。如果有两条以上的线程争用同一个锁,那轻 量级锁就不再有效,要膨胀为重量级锁。

  • 解锁过程:如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和和线程中复制的 Displaced Mark Word 替 换回来,如果替换成功,整个过程就完成。如果失败,说明有其他线程尝试过 获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。

  • 轻量级锁的依据:对于绝大部分的锁,在整个同步周期内都是不存在竞争 的。

  • 传统锁(重量级锁)使用操作系统互斥量来实现的。

HotSpot 虚拟机的对象的内存布局:对象头(Object Header)分为两部分信息吗,第 一部分(Mark Word)用于存储对象自身的运行时数据,另一个部分用于存储指向方法区 对象数据类型的指针,如果是数组的话,还会由一个额外的部分用于存储数组的长度。

32 位 HotSpot 虚拟机中对象未被锁定的状态下, Mark Word 的 32 个 Bits 空间中 25 位 用于存储对象哈希码,4 位存储对象分代年龄,2 位存储锁标志位,1 位固定为 0。

HotSpot 虚拟机对象头 Mark Word

存储内容标志位状态对象哈希码、对象分代年龄 01 未锁定指向锁记录的指针 00 轻量级锁定指向重量级锁的指针 10 膨胀(重量级锁)空,不记录信息 11GC 标记偏向线程 ID,偏向时间戳、对象分代年龄 01 可偏向

5. 偏向锁

  • 目的是消除在无竞争情况下的同步原语,进一步提高程序的运行性能。锁 会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线 程获取,则持有锁的线程将永远不需要再进行同步。

  • 当锁第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为 01, 同时使用 CAS 操作把获取到这个锁的线程的 ID 记录在对象的 Mark Word 之 中,如果成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,都可 以不进行任何同步操作。

  • 当有另一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象 目前是否处于被锁定的状态,撤销偏向后恢复到未锁定或轻量级锁定状态。

十. wait()和 sleep()的区别。

  1. 这两个方法来自不同的类,sleep()来自 Thread 类,是静态方法;wait() 是 Object 类里面的方法,和 notify()或者 notifyAll()方法配套使用,来实现 线程间的通信。

  2. 最主要是 sleep 是将当前线程挂起指定的时间,没有释放锁;而 wait 方法 释放了锁,使得其他线程可以使用同步控制块或者方法。

  3. 使用范围:wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块 里面使用,而 sleep 可以在任何地方使用 。

synchronized(x){ x.notify()  //或者 wait()}
复制代码

特别注意: sleep 和 wait 必须捕获异常(Thread.sleep()和 Object.wait() 都会抛出 InterruptedException), notify 和 notifyAll 不需要捕获异常。

十一. Java 中 interrupted() 和 isInterrupted()方法的区别?

二个方法都是判断线程是否停止的方法。

  1. 前者是静态方法,后者是非静态方法。interrupted 是作用于当前正在运 行的线程, isInterrupted 是作用于调用该方法的线程对象所对应的线程。 (线程对象对应的线程不一定是当前运行的线程。例如我们可以在 A 线程中去调用 B 线程对象的 isInterrupted 方法,此时,当前正在运行的线程就是 A 线程。)

  2. 前者会将中断状态清除而后者不会。

十二. Java 创建线程之后,直接调用 start()方法和 run()的区别 ?

  1. start()方法来启动线程,并在新线程中运行 run()方法,真正实现了 多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面 的代码;通过调用 Thread 类的 start()方法来启动一个线程,这时此线程是 处于就绪状态,并没有运行,然后通过此 Thread 类调用方法 run()来完成其 运行操作,这里方法 run()称为线程体,它包含了要执行的这个线程的内容, run ()方法运行结束,此线程终止。然后 CPU 再调度其它线程。

  2. 直接调用 run()方法的话,会把 run()方法当作普通方法来调用,会 在当前线程中执行 run()方法,而不会启动新线程来运行 run()方法。程序还 是要顺序执行, 要等待 run 方法体执行完毕后,才可继续执行下面的代码; 程 序中只有主线程——这一个线程, 其程序执行路径还是只有一条, 这样就没有 达到多线程的目的。

十三. 什么是线程的上下文切换?

对于单核 CPU,CPU 在一个时刻只能运行一个线程,当在运行一个线程的 过程中转去运行另外一个线程,这个叫做线程上下文切换(对于进程也是类似)。

线程上下文切换过程中会记录程序计数器、CPU 寄存器的状态等数据。

虽然多线程可以使得任务执行的效率得到提升,但是由于在线程切换时同 样会带来一定的开销代价,并且多个线程会导致系统资源占用的增加,所以在 进行多线程编程时要注意这些因素。

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

在 java.lang.Thread 中有一个方法叫 holdsLock(Object obj),它返回 true,如果当且仅当当前线程拥有某个具体对象的锁。


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

当我们在 Java 程序中创建一个线程,它就被称为用户线程。将一个用户线 程设置为守护线程的方法就是在调用 start()方法之前, 调用对象的 setDamon(true)方法。一个守护线程是在后台执行并且不会阻止 JVM 终止的 线程,守护线程的作用是为其他线程的运行提供便利服务。当没有用户线程在 运行的时候, JVM 关闭程序并且退出。一个守护线程创建的子线程依然是守护 线程。

守护线程的一个典型例子就是垃圾回收器。

十六. 什么是线程调度器?

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

十七. 线程的状态。

版本 1.

在 Java 当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 第一是创建状态。在生成线程对象,并没有调用该对象的 start 方法,这是 线程处于创建状态。

  • 第二是就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就 绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就 绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程, 此时线程就进入了运行状态,开始运行 run 函数当中的代码。

  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个事 件的发生(比如说某项资源就绪)之后再继续运行。sleep,wait 等方法都可以导 致线程阻塞。

  • 第五是死亡状态。如果一个线程的 run 方法执行结束或者异常中断后,该 线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。

版本 2.

一般来说,线程包括以下这几个状态:创建(new)、就绪(runnable)、运 行(running)、阻塞(blocked)、timed_waiting、waiting、消亡(dead)。



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

join()方法。

十九. 在一个主线程中,要求有大量子线程执行完之后,主线程才执行完成。多种方式,考虑效率。

1. 在主函数中使用 join()方法。

t1.start();t2.start();t3.start();t1.join(); //不会导致 t1 和 t2 和 t3 的顺序执行 t2.join();t3.join();System.out.println("Main finished");
复制代码

2. CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行 的操作之前,它允许一个或多个线程一直等待。

public class WithLatch { public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new ChildThead(i, latch).start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main finished"); } static class ChildThead extends Thread { private int id = -1; private CountDownLatch latch = null; public ChildThead(int id, CountDownLatch latch) { this.id = id; this.latch = latch; } public void run() { try { Thread.sleep(Math.abs(new Random().nextint(5000))); System.out.println(String.format("Child Thread %dfinished", id)); } catch (InterruptedException e) { e.printStackTrace(); } finally { latch.countDown(); } } }}
复制代码

3. 使用线程池。

public class WithExecutor { public static void main(String[] args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(3); List<Callable<Void>> list = new ArrayList<Callable<Void>>(); for (int i = 0; i < 3; i++) { list.add(new ChildThead(i)); } try { pool.invokeAll(list); } finally { pool.shutdown(); } System.out.println("Main finished"); } static class ChildThead implements Callable<Void> { private int id = -1; public ChildThead(int id) { this.id = id; } public Void call() throws Exception { try { Thread.sleep(Math.abs(new Random().nextint(5000))); System.out.println(String.format("Child Thread %dfinished", id)); } catch (InterruptedException e) { e.printStackTrace(); } return null; } }}
复制代码

写在最后

作为一名 Java 程序员,想进 BAT 只学多线程还远远不够!

想进 BAT,像 Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分布式、高并发、性能调优、微服务等架构技术你都要学习!

当然以上技术能够掌握百分之八九十的话,进阿里 P8 还是没什么大问题的!

那么笔者也针对以上技术整理了一份完整的面试资料(详见下图)

原文档打开方式:我的学习笔记


用户头像

程序知音

关注

还未添加个人签名 2022.06.25 加入

还未添加个人简介

评论

发布
暂无评论
不会多线程还想进BAT?精选19道多线程面试题,有答案边看边学_Java_程序知音_InfoQ写作社区