JAVA面试部分——后端-线程前篇

3.1 线程和进程

在计算机科学中,进程和线程是操作系统管理资源的两种不同方式。

  • 进程(Process):是程序在计算机上的一次执行活动。每个进程都有自己的内存空间,包括代码、数据和系统资源。一个进程可以包含多个线程。进程之间相互独立,各自拥有独立的内存空间和系统资源,彼此不会直接共享数据,通信需要通过进程间通信机制来实现。

  • 线程(Thread):是进程中的一个执行单元。一个进程中的多个线程共享相同的内存空间和系统资源,可以共享数据。线程在同一进程中运行,相对于进程而言更加轻量级,创建和销毁的开销更小,线程间的切换开销也更小。

Java 中的线程是通过 java.lang.Thread 类来实现的。Java 程序运行时会启动一个主线程(Main Thread),可以通过创建 Thread 对象来创建新的线程,并调用 start() 方法来启动线程的执行。

多线程可以充分利用多核处理器的计算能力,提高程序的执行效率。但同时,多线程编程也需要处理同步、死锁、竞态条件等并发问题,需要谨慎编写以确保线程安全。

3.2 线程的实现方式?

继承Thread类,实现Runnable接口,实现Callable接口,创建线程池。

3.3 有三个线程,制定一种机制,保证线程a在线程b之前运行,线程b在线程c之前运行
你可以使用线程同步工具来实现这种机制。其中最常见的是使用Java的wait()和notify()方法。

首先,为每个线程创建一个类,并在类中定义一个共享的Object,我们称之为syncObj。这个对象用于线程间的通信。

java
public class ThreadA extends Thread {  
    private Object syncObj;  
      
    public ThreadA(Object syncObj) {  
        this.syncObj = syncObj;  
    }  
  
    @Override  
    public void run() {  
        synchronized (syncObj) {  
            // 线程a的代码  
            System.out.println("Thread A is running");  
              
            // 通过notify唤醒线程b  
            syncObj.notify();  
        }  
    }  
}  
  
public class ThreadB extends Thread {  
    private Object syncObj;  
      
    public ThreadB(Object syncObj) {  
        this.syncObj = syncObj;  
    }  
  
    @Override  
    public void run() {  
        synchronized (syncObj) {  
            // 线程b的代码  
            System.out.println("Thread B is running");  
              
            // 通过notify唤醒线程c  
            syncObj.notify();  
        }  
    }  
}  
  
public class ThreadC extends Thread {  
    private Object syncObj;  
      
    public ThreadC(Object syncObj) {  
        this.syncObj = syncObj;  
    }  
  
    @Override  
    public void run() {  
        synchronized (syncObj) {  
            // 线程c的代码  
            System.out.println("Thread C is running");  
        }  
    }  
}
//然后在主程序中创建并启动这些线程:
​
java
public static void main(String[] args) {  
    Object syncObj = new Object();  
    ThreadA threadA = new ThreadA(syncObj);  
    ThreadB threadB = new ThreadB(syncObj);  
    ThreadC threadC = new ThreadC(syncObj);  
    threadA.start();  
    threadB.start();  
    threadC.start();  
}
这样,线程a会在其他线程之前运行,然后唤醒线程b,线程b再唤醒线程c。但是需要注意的是,Java的wait()和notify()方法在实际使用时需要非常小心,因为如果没有正确地使用它们,可能会导致死锁或者无法预料的后果。例如,如果线程b没有在调用notify()之前唤醒线程a,那么线程a可能会永远被阻塞。
3.4 Runnable与Callable的区别?

实现Callable接口的任务线程能返回执行结果;而实现Runnable接口的任务线程不能返回结果; Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。

3.5 list中有20条数据分为5个线程每个线程4个数据,每条数据+1,怎么拿到他们组合成一个list?

采用Callable的异步算法(call方法有返回值,run方法没有返回值)。

在Java中,你可以使用ExecutorServiceCallable来实现你的需求。Callable是一种可以返回结果的任务接口,它的对象可以被ExecutorService线程池执行。

import java.util.*;  
import java.util.concurrent.*;  
  
public class Main {  
    public static void main(String[] args) throws InterruptedException, ExecutionException {  
        // 假设的20条数据  
        List data = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20));  
  
        // 分成5组,每组4个数据  
        List> groups = new ArrayList<>();  
        for (int i = 0; i < data.size(); i += 4) {  
            groups.add(new ArrayList<>(data.subList(i, Math.min(data.size(), i + 4))));  
        }  
  
        // 创建一个固定大小的线程池  
        ExecutorService executor = Executors.newFixedThreadPool(5);  
  
        // 对每个组执行加1操作,并收集结果  
        List>> futures = new ArrayList<>();  
        for (List group : groups) {  
            futures.add(executor.submit(new Callable>() {  
                @Override  
                public List call() throws Exception {  
                    return processGroup(group);  
                }  
            }));  
        }  
  
        // 等待所有操作完成,并收集结果  
        List combinedList = new ArrayList<>();  
        for (Future> future : futures) {  
            combinedList.addAll(future.get());  
        }  
  
        System.out.println(combinedList); // 打印结果  
    }  
  
    private static List processGroup(List group) {  
        // 对每个元素加1操作  
        List result = new ArrayList<>();  
        for (Integer num : group) {  
            result.add(num + 1);  
        }  
        return result;  
    }  
}
3.6 线程池是什么?什么情况下需要使用线程池?使用线程池的好处

以下是一些需要使用线程池的情况:

  • 并发任务处理:线程池可以用于处理并发的任务,例如处理请求、批量处理数据、并行计算等。通过线程池,可以管理和复用线程,提高任务的执行效率。

  • 异步任务执行:线程池可以用于执行异步任务,将任务提交给线程池后,可以立即返回并继续执行后续代码,不必等待任务完成。适用于需要在后台执行耗时任务,同时不阻塞主线程的场景。

  • 定时任务调度:线程池提供了定时任务执行的功能,可以周期性地执行任务或在指定的时间点执行任务。适用于需要按计划执行任务的场景,例如定时任务、定时检查等。

  • 资源池管理:线程池可以用于管理资源池,例如数据库连接池、线程池等。通过线程池,可以复用和管理资源,提高资源利用率,同时控制资源的并发访问。

  • 并行计算:线程池可以用于并行计算,将计算任务分解为多个子任务,分配给线程池中的线程并行执行,加速计算过程。适用于需要高性能并行计算的场景,如数据处理、图像处理等。

  • 长时间运行的任务:线程池适用于长时间运行的任务,可以控制线程的生命周期,避免频繁创建和销毁线程的开销,提高系统的稳定性和性能。

总之,无论是在Web服务器、桌面应用程序还是大规模分布式系统中,线程池都可以提供更好的性能、可扩展性和资源管理。选择合适的线程池并进行适当的调优,可以有效地提高系统的性能和稳定性。

3.7 线程池:核心线程数,最大线程数,队列的选择,线程初始化的时候线程数量是怎么变化的?
  • 核心线程数:线程池中保持活动线程的最小数量。这些线程会在进程启动时创建,并且只要线程池需要,就会持续存在。核心线程数通常由corePoolSize属性设置。

  • 最大线程数:线程池中允许存在的最大线程数量。如果队列满了并且已经达到了核心线程数,那么线程池会根据需要创建更多的线程,直到达到这个最大值。最大线程数通常由maximumPoolSize属性设置。

  • 队列的选择:当有新的任务提交给线程池,而当前线程数未达到核心线程数时,这些任务会被放在一个队列中等待。常见的队列类型有:无界队列(如LinkedBlockingQueue)、有界队列(如ArrayBlockingQueue)以及优先队列(如PriorityBlockingQueue)等。队列的选择需要根据实际需求来决定。

    线程池的队列选择取决于多个因素,包括任务的特性、线程池的实现方式以及具体的应用场景。以下是关于几种常见队列的讨论:

    1. SynchronousQueue:一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者。如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。

    2. ArrayBlockingQueue:是一种有界队列,它实现了BlockingQueue接口。此队列按 FIFO(先进先出)原则对元素进行排序。此队列的容量必须指定,在创建后就不能改变。如果尝试向已满的队列中添加元素,操作就会阻塞。如果尝试从空的队列中获取元素但该队列已经不再有可用元素(即队列为空),则操作也会阻塞。

    3. LinkedBlockingQueue:是一种无界队列(LinkedBlockingQueue的容量仅受制于内存的大小),它实现了BlockingQueue接口。此队列按照 FIFO(先进先出)原则对元素进行排序。

    4. PriorityBlockingQueue:是一种无界队列,它实现了BlockingQueue接口。此队列按照元素的优先级对元素进行排序。如果多个元素具有相同的优先级,则将按照它们的自然顺序(例如它们的自然排序或者插入顺序)进行排序。

    5. 选择队列时需要考虑任务的数量、任务的优先级以及系统的内存情况等因素。例如,如果系统需要处理大量低优先级的任务,那么选择LinkedBlockingQueue可能更为合适;如果任务数量有限,那么可以选择ArrayBlockingQueue,这样可以避免队列过大导致的内存压力。另外,如果任务具有不同的优先级,可以选择PriorityBlockingQueue来保证高优先级的任务能够优先得到处理。

  • 线程初始化的时候线程数量:初始时,线程池中的线程数量通常等于核心线程数。如果任务提交的速度超过了核心线程的处理速度,那么队列会逐渐增长,直到达到最大队列长度。如果当前线程数已经达到了最大线程数,但队列已经满了,那么新提交的任务将会被拒绝。

在实际应用中,选择合适的线程池和参数配置需要根据具体的应用场景和需求来决定。例如,对于处理大量并发请求的Web应用,可以选择ThreadPoolExecutorForkJoinPool;对于需要处理大量计算任务的场景,可以选择ForkJoinPoolExecutorService等。

3.8 说一下常见的几个线程池?(Java里面有4个线程池)

常见的线程池有以下几种:

  • newSingleThreadExecutor():创建一个只有一个线程的线程池。这个线程池会保证所有的任务按照提交的顺序一个一个地执行。如果这个线程池中的线程因为任务异常结束,那么它会立即创建一个新的线程替代。

  • newFixedThreadPool():创建一个定长的线程池。这个线程池会一次性创建指定数量的线程,然后每次提交的任务都会被一个线程立即执行。如果所有线程都在执行任务,那么新的任务就会进入等待队列,等待有线程空闲时再执行。

  • newCacheThreadPool():创建一个可缓存的线程池。这个线程池的大小会根据需要动态调整。如果线程池的大小超过了处理任务所需的线程数,那么就回收部分线程(一般是60S内未执行)。

  • newScheduledThreadPool():创建一个定长的线程池,支持定时和周期性任务执行。

  • newSingleThreadScheduledExecutor():创建一个单例的线程池,支持定期或延时执行任务。

以上就是Java中常见的五种线程池,了解它们可以帮助我们更好地利用多线程处理任务,提高程序的执行效率。

3.9 线程怎么释放的?

线程池中的线程可以通过以下方式释放:

  • 执行器自动关闭旧线程:当线程池中的线程完成任务后,执行器会自动关闭状态为非运行状态的线程。

  • 线程自愿退出:在线程没有获取到任务,并且在指定的等待时间内也没有任务可执行时,线程会自愿退出,通过processWorkerExit()方法可以证实这一点。

  • 定时器触发线程回收:对于核心线程,如果长时间没有任务执行,定时器会触发回收线程。

需要注意的是,线程池中的线程数量没有固定,可以根据需要动态调整。当任务量增大时,线程池会自动增加线程数量;当任务量减少时,线程池会自动减少线程数量。同时,线程池中的线程状态也不同,有些是核心线程,有些是非核心线程。核心线程在空闲1分钟后也会被回收。

在Spring框架中,可以通过配置线程池来管理线程的释放。以下是一些常用的配置选项:
​
1.使用ThreadPoolTaskExecutor类创建线程池:
java
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();  
executor.setCorePoolSize(5);  
executor.setMaxPoolSize(10);  
executor.setKeepAliveSeconds(60);  
executor.setQueueCapacity(25);  
executor.setThreadNamePrefix("MyThreadPool-");  
executor.initialize();
​
2.在Spring的配置文件中配置线程池:
xml

​
3.在Spring的配置文件中配置ThreadPoolTaskExecutor:
xml

​
这些配置选项可以设置线程池的核心线程数、最大线程数、线程的存活时间、队列容量以及线程名称前缀等参数。根据实际需求,可以调整这些参数来优化线程池的性能。
3.10 为什么要线程同步?并说一说线程同步的方式

线程同步的目的是为了保证多个线程之间的协同工作,避免出现数据竞争和不一致的情况。线程同步可以确保在同一时刻只有一个线程可以访问共享资源,避免多个线程同时对同一份数据进行修改或操作,保证数据的一致性和完整性。

线程同步的方式有多种,常见的有以下几种:

  • 互斥锁(Mutex):通过使用互斥锁,只有一个线程可以在同一时刻持有这个锁,其他线程必须等待该线程释放锁之后才能访问共享资源。

  • 信号量(Semaphore):信号量是一种更高级的同步机制,可以用于控制多个线程对共享资源的访问。信号量可以初始化为一个特定的值,表示可以同时访问共享资源的线程数量。当线程访问共享资源时,需要获取一个信号量许可,当访问完成后释放许可,让其他线程可以继续访问。

  • 条件变量(Condition):条件变量允许线程等待某个条件满足后才继续执行。它可以让线程等待特定的信号或事件发生,而不会浪费CPU时间。

  • 读写锁(Read-Write Lock):读写锁是一种特殊的锁机制,适用于读操作比写操作更频繁的情况。它允许多个线程同时进行读操作,但只允许一个线程进行写操作。

  • 临界区(Critical Section):临界区是一段必须互斥执行的代码,用于保护共享资源的访问。只有拥有临界区的线程才能执行其中的代码,其他线程必须等待。

这些线程同步方式各有优缺点,应根据具体情况选择适合的同步方式。

3.11 线程池怎么从等待队列中拿任务的?(头结点)

线程池从等待队列中拿任务的核心过程如下:

  • 线程池首先会检查等待队列是否为空,如果等待队列为空,则无法从队列中获取任务。

  • 如果等待队列不为空,线程池会尝试从队列头部获取一个任务。

  • 如果无法从队列头部获取任务,说明队列已经被占用,这时线程池会尝试创建新的线程来执行任务。

  • 如果无法创建新的线程,线程池会采取拒绝策略来处理无法执行的任务。

通过以上步骤,线程池就可以从等待队列中获取任务并执行。

你可能感兴趣的:(#,后端,java,面试,python)