进程和线程的区别?
- 定义:进程是一个独立的程序执行实例,具有独立的内存空间、运行环境等;而线程是进程中的执行单元,共享进程的资源
- 资源占用:每个进程都会拥有自己的系统资源,如内存、文件、网络端口等,因此相对于线程来说,进程需要更多的系统资源。而线程只占用一小部分的内存,可以共享进程的资源
- 独立性:进程之间互不影响,需要通过进程间通信才能进行数据交换;而线程是共享进程资源的,它们之间可以直接访问共享变量或使用消息传递等方式进行通信
- 调度:进程是操作系统进行调度的基本单位,不同的进程之间需要进行切换,而线程则是在进程内部的调度单元,线程之间的切换比进程之间的切换快得多
- 安全性:由于线程共享进程的所有资源,如堆、栈、文件、数据库连接等,因此需要对这些资源进行同步处理,避免出现冲突和安全问题;而进程之间相互独立,不需要考虑这些问题
并发与并行的区别?
- 并发:多个线程同时操作同一个资源
- 并行:在多核CPU下多个线程可以同时执行
创建线程的几种方式?
- 继承Thread类并重写run()方法。这是一种最基本的方式,可以通过实例化一个Thread子类对象,并调用start()方法来启动线程
- 实现Runnable接口并重写run()方法。这种方式更常见,因为Java不支持多继承,如果你已经有另一个类需要继承,则只能通过实现Runnable接口来创建线程
- 使用Callable和Future接口。Callable是与Runnable类似的接口,也可以用于创建线程。不同之处在于,Callable接口的call()方法可以返回值,并且可以抛出异常。Future接口则表示一个异步计算的结果。通过Future.get()方法可以获取异步计算的结果
- 使用线程池。在Java中可以使用线程池来管理多个线程。线程池可以避免频繁地创建和销毁线程,从而提高程序的效率。 Java提供了Executor框架来创建和管理线程池
start()和run()的区别?
在Java中,start()和run()都是线程的启动方法,但它们之间有着显著的区别。
- start()方法
start()方法是Thread类提供的一个方法,用于启动一个新的线程,并让该线程执行run()方法中的代码。当调用start()方法时,会创建一个新的线程并使其进入就绪状态,等待被系统调度执行。需要注意的是,不能重复调用start()方法,否则会抛出IllegalThreadStateException异常。
- run()方法
run()方法是Thread类中定义的一个普通方法,用于存放线程执行的代码。当通过start()方法启动线程时,线程会自动调用run()方法中的代码。如果直接调用run()方法,则该方法只会在当前线程中执行,并不会真正启动一个新的线程。
因此,可以将start()方法看作是启动一个新线程的命令,而run()方法只是一个普通的方法,如果直接调用run()方法,则只是在当前线程中运行run()方法中的代码,并不会启动一个新的线程。
总结起来,start()方法是启动一个新的线程,而run()方法只是在线程中执行指定的代码块。一般情况下,使用start()方法启动线程,而不是直接调用run()方法。
Runnable和Callable的区别?
- 返回值:Runnable的run()方法没有返回值,而Callable的call()方法可以返回一个结果对象
- 异常处理:Runnable的run()方法不能抛出异常,而Callable的call()方法可以抛出异常。如果需要在执行过程中抛出异常,则需要使用Callable
- 使用方式:Runnable通常与Thread一起使用,而Callable通常与ExecutorService一起使用
- 等待时间:Callable的call()方法可以设置等待时间,在指定时间内没有获得结果,则会抛出TimeoutException
综上所述,Runnable适合于那些不需要返回值或者不需要抛出异常的场景,而Callable适合于那些需要返回值或者需要抛出异常的场景。同时,如果需要管理多个线程,建议使用ExecutorService来执行Callable任务
线程有几种状态?
线程的状态总共有6种,分别是:
- 新建(New):当线程对象被创建时,它处于新建状态
- 运行(Runnable):当调用start()方法后,线程进入可运行状态。但是此时线程不一定立即开始执行,需要等待CPU分配时间片之后才会开始执行
- 阻塞(Blocked):当线程等待某个资源或者锁时,当前线程就处于阻塞状态。比如等待用户输入、等待IO操作完成等
- 等待(Waiting):当线程等待某个特定条件满足时,它就处于等待状态。可以调用wait()、join()、park()等方法使线程进入等待状态
- 计时等待(Timed Waiting):当线程等待一个具有超时限制的特定条件时,它就处于计时等待状态。可以调用sleep()、wait(timeout)、join(timeout)、parkNanos()、parkUntil()等方法使线程进入计时等待状态
- 终止(Terminated):当run()方法正常结束或者抛出异常时,线程进入终止状态
wait()和sleep()的区别?
wait()和sleep()是Java中用于线程控制的两个方法,它们之间有以下区别:
- wait()方法属于Object类,而sleep()方法属于Thread类
- wait()方法必须在synchronized代码块或方法中调用,而sleep()方法可以在任何地方调用
- wait()方法会释放锁,而sleep()方法不会释放锁。当一个线程调用wait()方法时,它会释放持有的锁,并进入等待状态,直到其他线程调用notify()或notifyAll()方法来唤醒它
- wait()方法只能由其他线程调用notify()或notifyAll()方法来唤醒,而sleep()方法则需要等待指定的时间后自动唤醒
- 在使用wait()方法时,通常需要配合notify()或notifyAll()方法一起使用。notify()方法将唤醒一个处于等待状态的线程,而notifyAll()方法则会唤醒所有处于等待状态的线程
综上所述,wait()方法主要用于线程之间的协作,而sleep()方法主要用于暂停执行一段时间。同时,在使用wait()方法时需要注意避免死锁的问题
多线程同步有哪些方法?
- synchronized关键字:通过synchronized关键字来实现对象级别的锁定,阻塞其他线程的执行,直到获取到锁才能执行代码
- ReentrantLock类:与synchronized相似,但提供了更加灵活的锁控制功能
- volatile关键字:保证每个线程都能看到最新值,避免脏读等问题
- Atomic类:提供了一些原子操作,在多线程环境下保证了操作的原子性,避免了数据竞争
- CountDownLatch类:可以让某个线程等待其他线程完成后再执行
- CyclicBarrier类:可以让多个线程等待彼此达到某个状态后再执行
- 分布式锁
什么是死锁?
多个线程无限期地阻塞等待对方持有的锁资源,导致程序无法继续执行下去
如何避免死锁?
- 减少同步代码块中需要占用的锁数量,如果必须要占用多个锁,那么应该按照相同的顺序来获取锁
- 避免循环等待:通过定时等待、资源分配序列化等技术来避免多个线程长时间互相等待导致死锁
- 使用tryLock()方法来尝试获取锁,如果规定时间内没有获取到就返回false
- 使用可重入锁,例如ReentrantLock,在同一线程中可以重复获取锁,避免了死锁发生
- 让程序自动恢复,等待一段时间后尝试重新获取锁资源,以此来解除死锁
synchronized的几种用法?
- 锁类
- 锁方法
- 锁代码块
Fork/Join框架的作用?
大任务自动分散为小任务,并发执行,合并小任务结果
为什么需要线程池?
在Java中,线程池是一种重要的多线程处理技术,它能够高效地管理和复用线程资源,并且可以避免频繁地创建和销毁线程所带来的开销,从而提高程序的效率和性能
具体来说,线程池有以下几个优点:
- 提高程序的响应速度:线程池中的线程可以立即执行任务,无需等待线程创建的时间
- 提高系统的稳定性:线程池可以限制系统中并发执行的线程数量,防止因过多的线程导致系统资源耗尽而崩溃
- 提高资源利用率:线程池可以重复使用已经创建的线程,避免了重复创建线程所带来的开销,从而提高了系统的资源利用率
- 提高程序的可管理性:线程池可以统计线程的数量、完成的任务数、还剩的任务数等信息,便于监控和调整系统的运行状态
线程池的分类?
在Java中,线程池可以根据不同的特点进行分类,主要有以下几种:
- 固定大小线程池:固定大小线程池中的线程数量是固定的,即线程池一旦创建就无法进行扩容或缩容。这种线程池适合于任务数比较稳定的场景
- 可变大小线程池:可变大小线程池中的线程数量是根据当前任务数进行动态调整的。当任务数增加时,会增加线程数量;当任务数减少时,会自动缩减线程数量。这种线程池适合于任务数不稳定、变化范围比较大的场景
- 单线程池:单线程池中只有一个线程在执行任务。当该线程因为异常而退出时,会重新创建一个新的线程来继续执行任务。这种线程池适合于需要保证任务按照指定顺序依次执行的场景
- 定时任务线程池:定时任务线程池可以按照指定的时间间隔周期性地执行某项任务。这种线程池适合于需要定期执行任务的场景,比如定时备份数据、清理垃圾等
- 工作窃取线程池:工作窃取线程池是JDK7新增的一种线程池类型。它可以让空闲的线程从其他线程的队列中窃取任务来执行,从而提高线程利用率和系统性能
- Fork/Join线程池:Fork/Join线程池也是JDK7新增的一种线程池类型,它主要用于实现分而治之的算法,并行处理大规模数据集。Fork/Join线程池可以将一个大任务分割成多个小任务并行执行,最终合并结果返回
线程池核心参数?
在Java中,线程池的核心参数包括以下几个:
- corePoolSize:线程池的核心线程数,表示线程池可以容纳的最小线程数量
- maximumPoolSize:线程池的最大线程数,表示线程池可以容纳的最大线程数量
- keepAliveTime:线程空闲后存活时间,表示当线程池中的线程数量超过核心线程数时,多余的线程在空闲指定时间后会被回收
- unit:keepAliveTime的时间单位,比如秒、毫秒等
- workQueue:任务队列,用于存储等待执行的任务。常见的任务队列包括ArrayBlockingQueue、LinkedBlockingQueue等
- threadFactory:线程工厂,用于创建新的线程。可以通过实现ThreadFactory接口来自定义线程的创建方式,比如设置线程名、优先级等
- handler:拒绝策略,用于处理任务队列已满而无法继续添加任务的情况。常见的拒绝策略包括AbortPolicy、DiscardPolicy等
这些参数都是影响线程池性能和行为的重要因素,需要根据具体的应用场景进行调整。例如,在任务量不大但执行时间较长的场景中,可以适当增加corePoolSize和maximumPoolSize参数;在任务量较大且执行时间较短的场景中,则需要考虑使用较大的任务队列和合适的拒绝策略来保证系统稳定运行
线程池原理?
线程池的原理是将多个任务分配给一组线程来执行,从而避免了线程频繁创建和销毁所带来的开销。当一个任务到达时,如果当前线程池中有空闲线程,则将任务交给其中一个空闲线程来执行;如果当前线程池中没有空闲线程,则任务会被添加到任务队列中等待执行;当任务队列已满且当前线程数量还未达到最大线程数时,会创建新的线程来执行任务;当线程数量超过最大线程数时,会根据拒绝策略对任务进行处理
具体来说,线程池的工作流程如下:
- 当一个任务到达时,首先判断当前线程池中是否有空闲线程可用
- 如果有空闲线程,则将任务交给其中一个空闲线程来执行
- 如果没有空闲线程,则将任务添加到任务队列中等待执行
- 如果任务队列已满,并且当前线程数量还未达到最大线程数,则创建新的线程来执行任务
- 如果线程数量已达到最大线程数,并且任务队列已满,则根据拒绝策略对任务进行处理
- 当一个线程执行完任务后,会查看任务队列中是否还有待执行的任务,如果有则继续执行下一个任务,否则释放线程资源并进入空闲状态等待下一个任务
总之,通过合理配置线程池的参数,可以控制线程的数量、任务的排队方式以及任务的拒绝策略,从而提高系统的稳定性和性能。同时,线程池也能够防止因线程频繁创建和销毁所带来的开销,从而提高程序的效率和性能
拒绝策略?
在Java中,当线程池的任务队列已满但线程数量已达到最大值时,需要使用拒绝策略来处理无法处理的任务。Java提供了四种常见的拒绝策略:
- AbortPolicy:默认的拒绝策略,直接抛出RejectedExecutionException异常
- CallerRunsPolicy:将任务交给提交任务的线程来执行。也就是说,如果线程池中的线程都在忙碌,那么新的任务将由提交任务的线程来执行
- DiscardOldestPolicy:丢弃任务队列中最早添加的任务,并尝试重新提交当前任务
- DiscardPolicy:直接丢弃无法处理的任务,不做任何处理
可以根据具体的业务需求选择适合的拒绝策略。例如,AbortPolicy适用于对任务处理要求较高的系统,CallerRunsPolicy适用于任务比较轻量级的系统,DiscardPolicy适用于任务处理要求较低的系统。在使用拒绝策略时,需要注意避免因拒绝任务而导致严重的程序错误
如何关闭线程池?
在Java中,关闭线程池主要有以下几种方式:
- shutdown()方法:调用该方法可以平缓地关闭线程池,即不再接受新的任务,但会等待已提交的任务执行完毕后再关闭。这个方法将使得ThreadPoolExecutor的isShutdown()返回true
- shutdownNow()方法:调用该方法可以立即关闭线程池,并尝试停止正在执行的任务。这个方法将使得ThreadPoolExecutor的isShutdown()和isTerminated()都返回true
- awaitTermination()方法:该方法用于等待所有已提交的任务执行完毕后关闭线程池,它需要与shutdown()方法一起使用。这个方法接收一个超时时间参数,表示最多等待多长时间,如果等待超时则立即返回 一般情况下,推荐使用shutdown()方法来关闭线程池,因为这种方式比较平缓、安全,能够确保所有任务被处理完毕再关闭线程池。如果需要立即关闭线程池并且不需要等待任务执行完毕,可以使用shutdownNow()方法。同时,在调用shutdown()方法之后,可以配合awaitTermination()方法来等待任务执行完毕,从而保证线程池能够正确关闭
线程池阻塞队列的作用?
线程池的阻塞队列是用来存放等待执行的任务的,它的作用主要有以下几个方面:
- 缓冲作用:当任务提交到线程池时,如果当前没有空闲的线程可用,那么任务就会被放入阻塞队列中等待执行。因此,阻塞队列可以作为一个缓冲区,先将任务保存在队列中,等待线程池中的线程处理
- 控制任务数量:通过控制阻塞队列的大小,可以限制线程池最多可以处理的任务数量,从而避免线程数量过多导致系统资源浪费或者负载过高导致系统崩溃
- 提高系统稳定性:使用阻塞队列可以有效地平衡生产者和消费者之间的速度差异,缓解瞬时任务量的压力,保证系统正常运行,提高系统的稳定性
- 提高系统性能:由于阻塞队列可以缓存任务,当线程池中的线程处于忙碌状态时,无法立即处理新的任务,这时候可以通过阻塞队列来暂时保存任务,从而减少线程创建和销毁的开销,提高系统的性能
综上所述,线程池的阻塞队列可以起到一个缓冲区、任务调度器的作用,控制任务的数量、提高系统的稳定性和性能。不同的应用场景需要选择不同的阻塞队列实现,比如ArrayBlockingQueue适合于固定大小的线程池,LinkedBlockingQueue适合于动态大小的线程池,SynchronousQueue适合于较小的线程池等
为何先添加队列而不是先创建最大线程?
在Java中,线程池的设计思路是通过重复利用已经创建好的线程来处理任务,而不是频繁地创建和销毁线程。因此,在创建线程池时,先添加到阻塞队列中等待处理的任务数目应该尽可能多,并且在阻塞队列满了以后才考虑增加线程数量
一方面,如果过早地创建大量线程,会导致系统资源的浪费和操作系统负载的增加,影响系统的稳定性和性能。另一方面,合理利用阻塞队列可以平衡生产者和消费者之间的速度差异,缓解瞬时任务量的压力,保证系统正常运行,提高系统的稳定性
线程池中线程复用原理?
线程池中的线程复用原理是通过将已经创建好的线程分配给新的任务来避免频繁创建和销毁线程,从而提高系统的性能
具体来说,线程池在初始化时会创建一定数量的核心线程,这些线程可以一直存在,即使没有任务需要执行也不会被销毁,等待新的任务到来。当有新的任务提交到线程池时,线程池会检查当前是否有空闲的线程,如果有,则将任务交给其中一个空闲的线程去处理,否则任务会被添加到阻塞队列中等待处理
当有新的任务到来时,如果线程池中的线程数量小于最大线程数,则会创建新的线程来处理任务,直到线程数量达到最大线程数为止。如果线程池中的线程数量已经达到最大值,并且阻塞队列也已满,则根据拒绝策略对任务进行处理,例如抛出异常或者直接丢弃任务
当一个线程处理完一个任务后,它会从阻塞队列中获取下一个任务继续执行,这个过程中线程池会尽可能地复用已经创建好的线程,从而避免了频繁创建和销毁线程的开销,提高了系统的性能
总之,线程池的线程复用原理是通过将已经创建好的线程分配给新的任务来避免频繁创建和销毁线程,提高系统的性能和效率