Java并发常见面试题总结(上)

1、什么是线程和进程?

线程(Thread)和进程(Process)都是操作系统中用于实现多任务的概念,它们是操作系统中的基本执行单元。

进程是操作系统分配资源的基本单位,一个进程可以包含一个或多个线程。进程拥有独立的内存空间,一个进程的数据不能直接被另一个进程访问。进程之间通信需要使用特殊的机制,例如管道、消息队列、共享内存等。每个进程都拥有自己的地址空间、代码段、数据段、堆栈等,进程的切换开销比线程大,因为要保存和恢复进程的上下文。

线程是进程的执行单位,一个进程中可以有多个线程并行执行。线程共享进程的地址空间和其他资源,一个线程的数据可以直接被另一个线程访问。不同线程之间的通信和同步可以通过共享内存等机制来实现。线程的切换开销比进程小,因为不需要保存和恢复进程的上下文,只需要保存和恢复线程的上下文。

通常情况下,多线程的开发比多进程的开发更为常见,因为多线程的切换开销比多进程小,且线程之间共享进程的资源,通信更为方便。但是多进程的优点在于可以更好地利用多核CPU,提高系统的整体性能。

2、为什么要使用多线程?

使用多线程可以提高程序的运行效率和性能,其主要原因有以下几点:

  1. 利用多核CPU资源:现代计算机通常都是多核CPU,使用多线程可以将任务分配给不同的CPU核心并行执行,从而充分利用CPU资源,提高程序的执行效率和速度。

  2. 提高程序的响应速度:在单线程程序中,如果执行一个耗时的任务,程序会一直等待直到任务完成才能继续执行下一个任务,导致程序的响应速度变慢。使用多线程可以将耗时的任务放在一个单独的线程中执行,使得程序可以同时执行其他任务,从而提高程序的响应速度。

  3. 提高系统的并发能力:使用多线程可以实现多个任务的并发执行,从而提高系统的并发处理能力和吞吐量。

  4. 实现复杂的任务分解和协作:有些任务需要复杂的处理和协作,使用多线程可以将任务分解成多个子任务并行执行,再通过线程间的通信和协作实现整个任务的完成。

3、使用多线程可能带来什么问题?

  1. 线程安全问题:多线程访问共享的数据时,可能会出现数据竞争和不一致的问题,导致程序出现异常或不可预期的结果。需要通过锁、同步、原子操作等机制来保证线程安全。

  2. 死锁问题:当多个线程相互等待对方释放锁时,可能会出现死锁现象,导致线程无法继续执行,程序无法正常运行。

  3. 上下文切换开销问题:多线程切换时需要保存和恢复线程的上下文信息,这会带来一定的开销,如果线程切换过于频繁,会降低程序的运行效率。

  4. 线程资源占用问题:每个线程都需要占用一定的内存资源和CPU时间片,如果同时运行的线程数过多,可能会导致系统资源紧张,甚至导致系统崩溃。

  5. 调试和维护难度问题:多线程程序的调试和维护比单线程程序更加复杂,需要考虑多个线程之间的交互和状态变化,出现问题时也更难以定位和调试。

4、说说线程的生命周期和状态?

线程的生命周期主要包括以下状态:

  1. New(新建状态):当线程对象被创建时,处于新建状态。此时线程尚未启动,还未进入可运行状态。

  2. Runnable(可运行状态):当调用线程的 start() 方法后,线程进入可运行状态。此时线程已经准备好执行任务,但还未分配到 CPU 时间片。

  3. Running(运行状态):当线程获得 CPU 时间片并开始执行任务时,进入运行状态。此时线程正在执行自己的任务代码。

  4. Blocked(阻塞状态):当线程因为某种原因被阻塞时,进入阻塞状态。比如线程等待锁、等待输入输出、等待其他线程完成等。

  5. Waiting(等待状态):当线程因为某种原因需要等待一段时间时,进入等待状态。比如线程等待其他线程通知、等待定时器等。

  6. Timed Waiting(计时等待状态):当线程需要等待一段时间时,可以进入计时等待状态。比如线程等待定时器、等待 I/O 操作完成等。

  7. Terminated(终止状态):当线程执行完自己的任务代码后,进入终止状态。此时线程已经结束了自己的生命周期。

5、什么是线程死锁?如何避免死锁?

线程死锁是指两个或多个线程在等待其他线程释放资源时,彼此之间陷入了无限等待的状态,导致程序无法继续执行的情况。常见的死锁场景包括多线程互斥、多线程等待、多线程嵌套锁等。

为避免线程死锁,一般需要遵循以下原则:

  1. 避免使用嵌套锁:如果必须使用多个锁,应尽量避免嵌套使用锁,尽量将锁的粒度分解到最小,减少锁的争用和竞争。

  2. 统一加锁顺序:多个线程在竞争多个锁时,应该按照同一顺序加锁,避免出现相反的加锁顺序导致死锁的情况。

  3. 使用tryLock()方法:在使用 ReentrantLock 时,可以使用 tryLock() 方法来获取锁,如果获取失败可以及时释放资源,避免出现死锁。

  4. 设置超时时间:当使用 synchronized 或者 ReentrantLock 等锁时,可以设置超时时间,避免锁长时间被占用导致死锁。

  5. 避免饥饿和活锁:在线程互相等待的场景中,应尽量避免出现饥饿和活锁的情况,例如使用公平锁或者随机等待时间等方式。

6、什么是上下文切换?

上下文切换是指操作系统在进行多任务调度时,需要在不同任务之间切换上下文的过程。每个任务都需要保存自己的状态,包括寄存器、程序计数器、栈指针等等,这些状态被称为上下文。当一个任务被暂停执行时,它的上下文会被保存下来,等待下一次调度;当一个任务被恢复执行时,它的上下文会被重新加载,继续执行任务。

上下文切换是一种开销较大的操作,因为需要保存和加载大量的上下文数据,会消耗大量的 CPU 时间和内存资源,对系统性能和响应速度有很大影响。因此,在多任务调度时需要尽量减少上下文切换的次数,提高系统的性能和效率。

一般来说,可以通过以下几种方式来减少上下文切换的次数:

  1. 减少任务数:将任务数减少到最小,避免过多的任务调度和上下文切换。

  2. 调整任务优先级:根据任务的优先级和重要性,合理安排任务调度顺序,减少不必要的上下文切换。

  3. 使用协作式调度:在协作式调度中,任务自行判断何时主动让出 CPU 时间片,避免不必要的上下文切换。

  4. 使用异步 I/O:在异步 I/O 中,任务不需要阻塞等待 I/O 操作完成,而是将 I/O 操作交给内核处理,同时继续执行其他任务,减少阻塞和上下文切换。

7、synchronized 与 Lock 有什么异同

相似之处:

  1. 都能够实现对临界区的同步访问。

  2. 都可以用于实现互斥锁,确保同一时间只有一个线程能够访问临界区。

  3. 都能够保证线程之间的可见性。

不同点:

  1. synchronized 是 Java 语言级别的关键字,而 Lock 是一个接口,需要在代码中进行显式的调用和释放,即需要手动控制锁的获取和释放。

  2. synchronized 在获取锁时,如果锁已经被其他线程占用,当前线程会进入阻塞状态;而 Lock 可以通过 tryLock() 方法来获取锁,如果锁已经被其他线程占用,当前线程不会进入阻塞状态,而是返回 false。

  3. synchronized 只能实现非公平锁,而 Lock 可以实现公平锁和非公平锁,公平锁能够保证线程获取锁的顺序和请求锁的顺序一致。

  4. synchronized 可以用于实现监视器锁,支持 wait() 和 notify() 等方法,而 Lock 不支持。

  5. synchronized 是内置于 Java 虚拟机中的机制,所以性能相对较高,而 Lock 是基于 Java 代码实现的,性能相对较低。

总的来说,synchronized 更为简单,适用于大部分的场景,而 Lock 提供了更多的灵活性和功能,适用于一些特殊的场景。在实际开发中,应根据具体需求选择适当的同步机制。

8、如何实现 Java多线程?

  1. 继承 Thread 类

这种方式是最基本的多线程实现方式,继承 Thread 类并重写 run() 方法,在该方法中实现线程要执行的代码。然后创建该子类的实例,调用 start() 方法启动线程。

  1. 实现 Runnable 接口

这种方式比较灵活,实现 Runnable 接口并实现 run() 方法,在该方法中实现线程要执行的代码。然后创建该实现类的实例,将该实例作为参数传递给 Thread 类的构造方法,调用 start() 方法启动线程。

  1. 实现 Callable 接口

实现 Callable 接口并实现 call() 方法,在该方法中实现线程要执行的代码。然后创建该实现类的实例,将该实例作为参数传递给 FutureTask 类的构造方法,再将 FutureTask 类的实例作为参数传递给 Thread 类的构造方法,调用 start() 方法启动线程。

  1. 使用线程池

使用线程池可以避免频繁创建和销毁线程的开销,提高线程的执行效率和性能。Java 提供了 Executor 和 ExecutorService 接口来支持线程池的实现。

9、run()方法与 start()方法有什么区别?

在 Java 中,一个线程对象通过调用 start() 方法启动一个新线程,而这个新线程在运行时会自动调用 run() 方法来执行线程任务。

区别如下:

  1. start() 方法会启动一个新线程,并使该线程开始执行 run() 方法中的代码,而 run() 方法只是一个普通的方法调用,不会创建新的线程,直接在当前线程中运行。

  2. 在执行 start() 方法时,JVM 会为新线程分配一些资源,并将其加入到调度队列中,等待 CPU 调度执行;而直接调用 run() 方法,则不会创建新线程,而是直接在当前线程中执行 run() 方法中的代码。

  3. start() 方法是线程启动的标志,只有调用了 start() 方法才会启动一个新线程并执行 run() 方法;而直接调用 run() 方法则相当于在当前线程中普通的方法调用,不会启动新线程。

因此,如果需要启动新线程并执行线程任务,应该调用 start() 方法;如果只是想在当前线程中执行某个方法,可以直接调用 run() 方法。

10、sleep() 方法与 wait() 方法有什么区别?

在 Java 中,sleep() 方法和wait() 方法都可以用来让线程暂停执行一段时间,但它们之间有以下几个区别:

  1. wait() 方法是 Object 类中的方法,而 sleep() 方法是 Thread 类中的方法。因此,wait() 方法必须在 synchronized 块或方法中调用,而 sleep() 方法可以在任何地方调用。

  2. wait() 方法会释放锁,而 sleep() 方法不会。当一个线程调用 wait() 方法时,它会释放对象锁,使得其他线程可以获得该对象锁并执行相应的同步方法或块。而调用 sleep() 方法时,线程会一直持有对象锁,直到睡眠时间结束或被其他线程中断。

  3. wait() 方法是用于线程间通信的,它会使得当前线程进入等待状态,直到其他线程调用 notify() 或 notifyAll() 方法来唤醒它。而 sleep() 方法是用于让线程休眠一段时间,不需要其他线程的干预。

  4. wait() 方法和 sleep() 方法在等待时间到达时的处理方式不同。当等待时间到达时,wait() 方法不会自动返回,而是需要其他线程调用 notify() 或 notifyAll() 方法来唤醒它;而 sleep() 方法会自动返回,不需要其他线程干预。

因此,如果需要暂停线程并等待其他线程的干预,应该使用 wait() 方法;如果仅仅是需要让线程暂停一段时间,可以使用 sleep() 方法。另外,需要注意的是,wait() 和 notify() 方法只能在 synchronized 块或方法中使用,否则会抛出 IllegalMonitorStateException 异常。

11、sleep() 与 yield() 的区别?

在 Java 中,sleep() 方法和yield() 方法都可以用来控制线程的执行,但它们之间有以下几个区别:

  1. sleep() 方法会让当前线程暂停一段时间,并进入阻塞状态,直到指定的时间过去或者被其他线程中断。而 yield() 方法只是让出当前线程的 CPU 执行时间,让 CPU 调度器重新调度线程执行。

  2. sleep() 方法指定的时间可以比实际暂停的时间长,但不会比实际暂停的时间短;而 yield() 方法只是建议 CPU 调度器让出当前线程的执行时间,并不能保证一定会让出。

  3. sleep() 方法是静态方法,可以直接通过 Thread 类来调用;而 yield() 方法是实例方法,需要通过当前线程对象来调用。

  4. sleep() 方法会让线程进入阻塞状态,可能会导致线程切换的开销较大;而 yield() 方法只是让线程暂停一下,不会进入阻塞状态,因此开销较小。

因此,如果需要让线程暂停一段时间,可以使用 sleep() 方法;如果需要让出当前线程的 CPU 执行时间,可以使用 yield() 方法。需要注意的是,由于 yield() 方法不能保证让出 CPU 执行时间,因此不应该将其作为线程之间的同步机制使用。

12、join()方法的作用是什么?

  1. 等待调用该方法的线程执行完毕。当一个线程对象调用 join() 方法时,当前线程会被阻塞,直到调用该方法的线程执行完毕或者超时。

  2. 等待一段时间。join() 方法还可以带一个参数,表示等待的时间,超过这个时间后即使线程还没有执行完毕,当前线程也会继续执行。

  3. 等待指定线程组中所有线程执行完毕。如果调用 join() 方法的线程对象属于一个线程组,那么可以将 join() 方法的参数设置为 null,表示等待该线程组中的所有线程执行完毕。

通过使用 join() 方法,可以实现线程之间的同步,等待一个线程执行完毕后再执行下一个线程。这在一些多线程协作的场景中很常见,比如主线程等待子线程执行完毕后再继续执行。需要注意的是,join() 方法会让当前线程进入阻塞状态,因此应该在适当的地方使用它,避免影响程序的性能。

13、终止线程的方法有哪些?

  1. 使用 stop() 方法。stop() 方法是 Thread 类提供的一个实例方法,可以直接终止一个线程。但是,由于该方法可能会导致线程的资源没有得到释放,从而引起一些问题,因此已经被标记为 deprecated,不再推荐使用。

  2. 使用 interrupt() 方法。interrupt() 方法是 Thread 类提供的一个实例方法,可以中断一个线程。调用该方法后,线程的中断标志会被设置为 true,但并不会直接终止线程。需要在线程中通过检查中断标志来判断是否需要退出线程。

  3. 使用 volatile 标志位。可以通过设置一个 volatile 类型的标志位来控制线程是否继续执行。在线程中不断地检查该标志位,如果被设置为 true,则退出线程。

  4. 使用自定义的退出标志。可以在线程中设置一个自定义的标志位,控制线程是否继续执行。在线程中不断地检查该标志位,如果被设置为 true,则退出线程。

需要注意的是,以上方法都是通过设置一个标志位或者中断标志来控制线程是否继续执行,而不是直接终止线程。这样可以保证线程在退出时能够正确地释放资源,避免引起一些问题。因此,在编写多线程程序时应该避免使用 stop() 方法,而是使用 interrupt() 方法或者自定义的退出标志来终止线程。

14、什么是守护线程?

在 Java 中,线程可以分为两种类型:用户线程和守护线程。其中,守护线程(Daemon Thread)是一种特殊的线程,用于为其他线程提供服务,不会阻止程序的结束。

具体来说,当一个 Java 程序中只剩下守护线程时,程序就会自动退出。这是因为,JVM 会在退出之前检查程序中是否还有正在运行的线程,如果只剩下守护线程了,就会自动将它们停止,然后退出程序。

守护线程通常用于执行一些后台任务,比如垃圾回收、自动保存、自动更新等。这些任务不需要阻止程序的结束,因此可以使用守护线程来执行。在创建一个线程时,可以通过设置 setDaemon(true) 方法将它设置为守护线程。需要注意的是,一旦一个线程被设置为守护线程,就无法将它重新设置为用户线程。

需要注意的是,守护线程并不是用于执行一些重要的任务的,因为它们随时可能被停止。因此,在创建守护线程时需要特别小心,确保它们不会对程序的正确性产生影响。

15、为什么 wait() 方法不定义在 Thread 中?

Java 中的 wait() 方法是 Object 类的一个实例方法,不是 Thread 类的实例方法,因为等待和通知不仅仅是线程之间的问题,而是所有对象之间的问题。

wait() 方法的作用是使当前线程进入等待状态,并释放对象的锁。调用 wait() 方法的线程会进入等待队列,等待其他线程调用该对象的 notify() 或者 notifyAll() 方法,从而被唤醒。当一个线程被唤醒后,它会重新尝试获取对象的锁,如果成功获取锁,就可以继续执行,否则就会继续等待。

需要注意的是,调用 wait() 方法的线程必须先获得对象的锁,否则会抛出 IllegalMonitorStateException 异常。因此,wait() 方法必须定义在 Object 类中,而不是 Thread 类中。

 

gzh:大雄说技术,欢迎关注!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值