多线程
多线程(并发编程)
1、进程和线程的区别?
进程(Process)和线程(Thread)是操作系统中用于管理和执行程序的两个重要概念,它们之间有以下主要区别:
-
定义:
- 进程是程序的一次执行过程,是程序运行时的一个实例。它拥有独立的内存空间,包括代码段、数据段、堆栈段等。
- 线程是进程中的一个执行单元,是 CPU 调度的基本单位。一个进程可以包含多个线程,它们共享进程的内存空间和资源。
-
资源分配:
- 进程拥有独立的内存空间和系统资源,每个进程之间相互独立,互不影响。进程之间通信需要通过进程间通信(IPC)机制。
- 线程属于同一进程,共享进程的内存空间和资源,包括代码段、数据段、文件描述符等。线程之间可以直接共享数据,通信更加方便快捷。
-
切换开销:
- 由于进程拥有独立的内存空间,进程之间的切换开销较大,涉及到上下文的切换、页表的切换等操作。
- 线程属于同一进程,线程之间的切换开销较小,因为线程共享进程的资源,只需切换线程的上下文即可。
-
并发性:
- 多进程之间可以实现并发执行,每个进程有自己的执行流程,可以同时执行多个任务。
- 多线程之间也可以实现并发执行,由于线程共享进程的资源,线程之间的切换开销较小,可以更高效地实现并发执行。
-
适用场景:
- 进程适合于多任务之间相互独立、互不影响的场景,例如多个独立的程序同时运行。
- 线程适合于需要共享资源、并发执行的场景,例如在一个程序中同时执行多个任务。
总的来说,进程和线程是操作系统中用于管理和执行程序的两个重要概念。进程是程序的一次执行过程,拥有独立的内存空间和资源;线程是进程中的一个执行单元,共享进程的资源,可以实现更高效的并发执行。进程和线程各有适用的场景和优势,可以根据具体需求选择合适的方式来进行程序设计和管理。
2、什么是原子性、可见性、有序性?
原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)是并发编程中重要的概念,用于描述并发操作的特性和行为。下面对这三个概念进行详细解释:
-
原子性(Atomicity):
- 原子性指的是一个操作是不可分割的,要么全部执行成功,要么全部不执行,不存在中间状态。在并发环境下,原子操作可以保证多个线程同时访问时不会发生数据竞争和不一致的情况。
- 原子性的实现可以通过锁、CAS(Compare and Swap)等方式来保证。例如,Java 中的原子操作类 AtomicInteger、AtomicLong 等可以保证其操作的原子性。
-
可见性(Visibility):
- 可见性指的是当一个线程对共享变量进行修改后,其他线程能够立即看到这个修改后的值。在并发环境下,如果没有保证可见性,可能会导致一个线程对共享变量的修改对其他线程不可见,从而引发数据不一致的问题。
- 可见性的实现可以通过使用 volatile 关键字来保证。当一个变量被 volatile 修饰时,对该变量的写操作会立即刷新到主内存,对该变量的读操作也会从主内存中获取最新的值,从而保证可见性。
-
有序性(Ordering):
- 有序性指的是程序的执行顺序与代码的书写顺序保持一致。在并发环境下,如果没有保证有序性,可能会导致指令重排的问题,使得程序的执行顺序与预期不一致。
- 有序性的实现可以通过使用 synchronized、Lock 等锁机制来保证。这些锁机制可以保证代码块的执行顺序与代码的书写顺序保持一致,从而保证程序的有序性。
总的来说,原子性是指操作的不可分割性,可见性是指共享变量对其他线程的修改能够立即可见,有序性是指程序的执行顺序与代码的书写顺序保持一致。在并发编程中,要注意保证这三个特性,以确保程序的正确性和稳定性。
3、为什么要使用多线程?
使用多线程的主要目的是提高程序的并发性和性能,实现更高效的资源利用和任务处理。以下是使用多线程的一些主要原因:
-
提高程序的响应速度:
- 使用多线程可以将耗时的任务和计算密集型的操作放在单独的线程中执行,不会阻塞主线程的执行,从而提高程序的响应速度和用户体验。
-
提高系统的吞吐量:
- 多线程可以同时处理多个任务,有效利用 CPU 和其他资源,提高系统的吞吐量和并发能力,可以更快地完成任务和处理请求。
-
实现并发编程:
- 多线程是实现并发编程的重要手段,可以同时处理多个任务和请求,实现任务的并行执行和资源的共享利用。
-
提高资源利用率:
- 多线程可以更有效地利用计算资源、内存资源和网络资源,提高系统的资源利用率,减少资源的闲置和浪费。
-
改善程序结构:
- 使用多线程可以将任务和功能模块分解成多个独立的线程,使程序结构更清晰、模块化,易于维护和扩展。
-
实现异步操作:
- 多线程可以实现异步操作,例如通过多线程处理网络请求、文件读写等 I/O 操作,不会阻塞主线程的执行,提高程序的并发性和性能。
-
提高系统的稳定性:
- 多线程可以实现任务的分离和隔离,一个线程出现问题不会影响其他线程的执行,提高系统的稳定性和容错能力。
总的来说,使用多线程可以提高程序的并发性和性能,实现更高效的资源利用和任务处理,改善程序结构和用户体验,提高系统的稳定性和吞吐量,是现代软件开发中常用的技术手段之一。
4、创建线程有哪几种方式?
在 Java 中,创建线程有多种方式,主要包括以下几种:
-
继承 Thread 类:
- 创建线程的一种常见方式是通过继承
Thread
类,并重写run()
方法来定义线程的执行逻辑。然后通过创建线程对象并调用start()
方法来启动线程。
- 创建线程的一种常见方式是通过继承
-
实现 Runnable 接口:
- 另一种创建线程的方式是实现
Runnable
接口,并实现其run()
方法来定义线程的执行逻辑。然后通过创建线程对象,并将实现了Runnable
接口的对象作为参数传递给Thread
类的构造方法来创建线程,并调用start()
方法启动线程。
- 另一种创建线程的方式是实现
-
使用匿名类:
- 除了创建具体的
Thread
子类或实现Runnable
接口的类,还可以使用匿名类的方式来创建线程,这种方式适用于简单的线程逻辑。
- 除了创建具体的
-
使用 Lambda 表达式:
- Java 8 引入了 Lambda 表达式,可以更简洁地定义线程的执行逻辑。
-
实现
Callable
接口。Callable
接口是 Java 5 引入的,它与Runnable
接口类似,都可以用于定义线程执行的任务,但是Callable
接口的call()
方法可以返回结果或者抛出异常。
- 使用线程池:
- Java 提供了线程池(ThreadPoolExecutor)来管理线程,可以通过 Executors 工厂类创建不同类型的线程池,并提交任务给线程池执行,避免频繁创建和销毁线程的开销。
总的来说,创建线程的方式有继承 Thread
类、实现 Runnable
接口、使用匿名类、使用 Lambda 表达式以及使用线程池等多种方式,可以根据具体的需求和场景选择合适的方式来创建和管理线程。
5、什么是守护线程?
守护线程(Daemon Thread)是一种特殊类型的线程,它主要用于为其他线程提供服务和支持。与普通线程(用户线程)相比,守护线程具有以下几个特点:
-
服务性质:
- 守护线程通常用于为其他线程提供服务和支持,例如垃圾回收线程就是守护线程,它负责回收无用对象释放内存空间,为应用程序的正常运行提供支持。
-
后台执行:
- 守护线程是在后台运行的线程,它们不会阻止 JVM 退出。当所有的用户线程执行完毕或者主线程结束时,守护线程也会随之结束。
-
特殊标识:
- 守护线程可以通过
setDaemon(true)
方法设置为守护线程。如果不显式设置,默认情况下,线程是普通线程(用户线程)。
- 守护线程可以通过
-
资源释放:
- 当 JVM 中只剩下守护线程时,JVM 会自动退出。因此,守护线程在完成任务后会自动释放资源并结束执行,不会造成资源的浪费。
守护线程适合用于执行一些后台任务和服务性的工作,例如定时任务、垃圾回收、日志记录等。但需要注意的是,守护线程在执行过程中不应该持有必须关闭的资源(如文件或数据库连接),因为守护线程的生命周期是不可控的,可能会在任何时候被 JVM 自动结束。
6、线程的状态有哪几种?怎么流转的?
线程在 Java 中有多种状态,主要包括以下几种:
-
新建状态(New):
- 当创建了一个线程对象但还没有调用
start()
方法启动线程时,线程处于新建状态。
- 当创建了一个线程对象但还没有调用
-
就绪状态(Runnable):
- 当调用了线程对象的
start()
方法后,线程处于就绪状态。就绪状态的线程已经被加入到线程池中,但还没有开始执行,等待 CPU 调度执行。
- 当调用了线程对象的
-
运行状态(Running):
- 当 CPU 调度执行了就绪状态的线程时,线程进入运行状态。处于运行状态的线程正在执行任务或代码。
-
阻塞状态(Blocked):
- 线程在某些情况下可能会进入阻塞状态,如等待 I/O 操作、等待获取锁、等待其他线程的通知等。线程在阻塞状态下暂时停止执行,直到条件满足后再转入就绪状态。
-
等待状态(Waiting):
- 线程调用
Object.wait()
方法或者Thread.join()
方法时,会进入等待状态。处于等待状态的线程需要等待其他线程的通知或者等待一定时间后才能转入就绪状态。
- 线程调用
-
超时等待状态(Timed Waiting):
- 线程调用带有超时参数的
Object.wait()
方法、Thread.sleep()
方法或者Thread.join(long millis)
方法时,会进入超时等待状态。处于超时等待状态的线程会在一定时间后自动转入就绪状态。
- 线程调用带有超时参数的
-
终止状态(Terminated):
- 线程执行完任务或者调用了
Thread.stop()
方法终止线程后,线程进入终止状态。终止状态的线程生命周期结束,不再参与 CPU 调度。
- 线程执行完任务或者调用了
线程的状态流转如下:
- 新建状态 -> 就绪状态:调用
start()
方法将线程加入线程池中等待 CPU 调度。 - 就绪状态 -> 运行状态:CPU 调度执行线程任务或代码。
- 运行状态 -> 就绪状态:线程执行完任务或者调用
yield()
方法主动放弃 CPU 资源。 - 运行状态 -> 阻塞状态、等待状态、超时等待状态:线程等待 I/O 操作、等待获取锁、等待其他线程通知或者等待一定时间。
- 阻塞状态、等待状态、超时等待状态 -> 就绪状态:条件满足或等待时间结束后,线程重新进入就绪状态等待 CPU 调度。
- 就绪状态 -> 终止状态:线程执行完任务后或者调用
Thread.stop()
方法终止线程。
线程的状态流转由 JVM 调度器(Scheduler)负责控制,根据线程的优先级和调度算法决定线程的执行顺序和状态转换。
7、线程的优先级有什么用?
线程的优先级(Priority)用于指定线程在竞争 CPU 资源时的调度优先级,优先级高的线程在竞争到 CPU 时具有较高的执行权重,有助于提高其被调度执行的机会。Java 中线程的优先级范围是 1 到 10,默认优先级为 5。线程的优先级有以下几个作用:
-
调度器的参考:
- 线程的优先级是调度器(Scheduler)进行线程调度的一个参考标准。调度器倾向于优先调度优先级高的线程,使其获得更多的 CPU 时间片来执行任务。
-
优化性能:
- 将一些重要或者紧急的任务分配给优先级高的线程,可以优化程序的性能和响应速度,确保重要任务能够及时得到处理。
-
避免饥饿:
- 通过调整线程的优先级可以避免线程的饥饿现象,即优先级低的线程长时间得不到 CPU 资源执行的情况。
-
控制并发性:
- 在一些并发编程的场景中,通过设置线程的优先级可以控制线程之间的竞争关系,例如让核心功能的线程优先执行,避免其他任务对核心功能的影响。
需要注意的是,线程的优先级虽然可以影响线程的调度顺序,但并不是绝对的,调度器并不保证优先级高的线程一定会在优先级低的线程之前执行。实际上,线程优先级的影响因素还包括调度器的实现、操作系统的调度算法等因素,因此不能过度依赖线程优先级来实现程序的精确控制。在编写多线程程序时,应当合理设计线程之间的协作与同步,避免过分依赖线程优先级来控制程序的执行顺序。
8、我们常说的 JUC 是指什么?
JUC 是 Java 并发工具包(Java Util Concurrent)的缩写。Java 并发工具包是 Java 5(JDK 1.5)引入的一个用于支持多线程编程的工具包,提供了一系列并发编程中常用的工具类、线程池、原子变量、同步器等组件,用于简化并发编程的开发和管理。JUC 的主要目的是帮助开发者更容易地编写高效、线程安全的并发程序。
JUC 包含了多个子包,其中常用的包括:
-
java.util.concurrent:包含了并发编程中常用的工具类和接口,如 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue 等。
-
java.util.concurrent.atomic:提供了原子变量类,如 AtomicInteger、AtomicLong 等,用于支持无锁的原子操作。
-
java.util.concurrent.locks:包含了显式锁(Explicit Lock)的实现类,如 ReentrantLock、ReadWriteLock 等,用于替代 synchronized 关键字进行更灵活的同步控制。
-
java.util.concurrent.atomic:提供了原子变量类,如 AtomicInteger、AtomicLong 等,用于支持无锁的原子操作。
-
java.util.concurrent.atomic:提供了原子变量类,如 AtomicInteger、AtomicLong 等,用于支持无锁的原子操作。
JUC 的引入极大地简化了 Java 并发编程的复杂度,提供了更高级别、更灵活的并发编程工具,帮助开发者编写高效、安全的并发程序。因此,JUC 是 Java 并发编程中非常重要的一个组成部分。
9、i++ 是线程安全的吗?
i++
操作本身不是线程安全的,因为它包含了读取、修改、写回三个步骤,而这些步骤并非原子操作。具体来说,i++
操作可以分解为以下步骤:
- 读取
i
的当前值。 - 对
i
的值加 1。 - 将加 1 后的值写回
i
。
如果多个线程同时对同一个变量执行 i++
操作,就可能会发生竞态条件(Race Condition),导致结果不确定或者不符合预期。例如,两个线程同时读取 i
的值为 5,然后各自加 1 得到 6,然后写回 i
,结果 i
的值只增加了 1,而不是期望的 2。
为了保证 i++
操作的线程安全性,可以采用以下方式之一:
-
使用同步机制:
- 使用 synchronized 关键字或者显式锁(如 ReentrantLock)来保护
i++
操作,确保同一时刻只有一个线程能够执行该操作。
- 使用 synchronized 关键字或者显式锁(如 ReentrantLock)来保护
-
使用原子类:
- 使用 Java 并发工具包中的原子类(AtomicInteger、AtomicLong 等)来替代普通的整型变量,原子类提供了线程安全的原子操作。
-
使用线程安全的集合:
- 如果
i
是作为集合的索引或者计数器使用,可以考虑使用线程安全的集合类,如 ConcurrentHashMap 的compute()
方法。
- 如果
这些方式可以保证 i++
操作的线程安全性,避免了多线程并发修改同一个变量导致的竞态条件问题。
10、join 方法有什么用?什么原理?
join()
方法是 Thread 类提供的一个方法,它的作用是让当前线程等待调用 join()
方法的线程执行完毕后再继续执行。换句话说,join()
方法会使当前线程阻塞,直到被调用的线程执行完毕或者达到指定的超时时间。
join()
方法的原理是通过线程间的协作来实现等待。当一个线程调用另一个线程的 join()
方法时,它会进入等待状态,等待被调用线程执行完毕。被调用线程执行完毕后,会通知等待的线程继续执行。
join()
方法通常用于需要等待其他线程执行完毕后再进行后续操作的场景,例如主线程需要等待所有子线程执行完毕后再进行汇总或者打印结果。另外,join(long millis)
方法还可以指定一个超时时间,表示最多等待指定的时间后就继续执行,无论被调用的线程是否执行完毕。
以下是 join()
方法的一些常见用法:
在上面的示例中,主线程通过调用 join()
方法等待 t1 和 t2 线程执行完毕后再继续执行后续操作,从而实现了线程间的协作和同步。
11、如何让一个线程休眠?
要让一个线程休眠,可以使用以下两种方式:
-
使用 Thread 类的 sleep() 方法:
- Thread 类提供了静态的
sleep(long millis)
方法,可以让当前线程休眠指定的时间(以毫秒为单位)。在休眠期间,线程会暂时停止执行,让出 CPU 资源给其他线程使用。
- Thread 类提供了静态的
-
使用 TimeUnit 类的 sleep() 方法:
- TimeUnit 是 Java 并发工具包中的一个枚举类,它提供了 sleep() 方法用于让线程休眠指定的时间。这种方式比直接使用 Thread 类的 sleep() 方法更加可读性好,因为可以通过 TimeUnit 的静态方法来指定时间单位。
在上面的代码中,Thread.sleep(1000)
和 TimeUnit.SECONDS.sleep(1)
都可以让当前线程休眠 1 秒钟。注意,在使用休眠方法时,需要处理可能抛出的 InterruptedException 异常,这是因为休眠过程中线程可以被其他线程中断,例如通过调用线程的 interrupt() 方法。因此,在捕获 InterruptedException 异常时,通常会添加相应的处理逻辑,例如重新设置中断状态或者抛出异常。
12、启动一个线程是用 start 还是 run 方法?
当涉及Java多线程时,了解如何正确启动一个线程是非常重要的。在Java中,可以通过继承Thread类或实现Runnable接口来创建线程。无论哪种方式,启动线程都应该使用start
方法,而不是直接调用run
方法。
-
使用
start
方法启动线程:-
在继承Thread类的情况下,重写
run
方法,并通过创建Thread对象然后调用其start
方法来启动线程。 -
在实现Runnable接口的情况下,实现
run
方法,并通过创建Thread对象传入实现了Runnable接口的类的实例来启动线程。
-
-
不要直接调用
run
方法:-
直接调用
run
方法不会创建新线程,而是在当前线程中执行run
方法的代码。 -
直接调用
run
方法不会实现多线程并发执行,而是在当前线程的上下文中执行run
方法中的代码。
-
总结来说,通过使用start
方法来启动线程,可以实现多线程并发执行,而直接调用run
方法则只会在当前线程中执行run
方法的代码,不会创建新线程。
13、start 和 run 方法有什么区别?
start
方法和run
方法是Java多线程中的两个重要方法,它们之间有着明显的区别:
-
start
方法:start
方法用于启动一个新线程,并在新线程中执行线程的run
方法。- 调用
start
方法会创建一个新的线程,并在新线程中执行run
方法中的代码。 start
方法返回后,线程处于就绪状态,等待获取CPU时间片后开始执行。- 如果一个线程已经启动过一次,再次调用该线程对象的
start
方法会抛出IllegalThreadStateException
异常。
-
run
方法:run
方法是Thread
类或者实现了Runnable
接口的类中的一个普通方法。- 直接调用
run
方法并不会创建新线程,而是在当前线程中顺序执行run
方法中的代码。 - 调用
run
方法会在当前线程的上下文中执行run
方法中的代码,不会创建新线程。
下面是一个示例,演示了start
方法和run
方法的区别:
在上面的示例中,调用start
方法会创建一个新线程并执行其中的代码,而直接调用run
方法则会在当前线程中执行run
方法的代码。
14、sleep 和 wait 方法有什么区别?
sleep
方法和wait
方法是Java中用于线程控制的两个重要方法,它们之间有以下区别:
-
作用对象:
sleep
方法是Thread
类的静态方法,用于使当前线程休眠一段时间。wait
方法是Object
类的方法,用于在线程间进行等待和唤醒操作。
-
调用方式:
sleep
方法可以直接通过线程对象或类名调用,如Thread.sleep(1000)
。wait
方法必须在同步代码块或同步方法中调用,因为它需要释放对象的锁,如object.wait()
。
-
使用场景:
sleep
方法通常用于暂停当前线程的执行,常用于模拟延迟、定时任务等。wait
方法通常用于多线程间的协作,例如等待某个条件满足后再继续执行,通常结合notify
或notifyAll
方法一起使用。
-
释放锁:
sleep
方法不会释放对象的锁,线程休眠时仍然持有锁。wait
方法在等待时会释放对象的锁,允许其他线程访问该对象的同步代码块或同步方法。
-
唤醒方式:
sleep
方法在指定时间后会自动唤醒,或者可以调用interrupt
方法手动唤醒。wait
方法需要通过调用对象的notify
或notifyAll
方法来唤醒等待的线程。
下面是一个简单的示例,演示了sleep
方法和wait
方法的区别:
在上面的示例中,线程A使用sleep
方法休眠2秒钟后自动唤醒,而线程B使用wait
方法等待,需要通过调用lock.notify()
或lock.notifyAll()
来唤醒。
15、Thread.yield 方法有什么用?
Thread.yield()
方法是Java中的一个静态方法,用于提示当前线程愿意让出CPU执行时间片,让其他具有相同优先级的线程有机会执行。但它不能保证其他线程一定会得到执行的机会,因为线程调度依赖于操作系统和虚拟机的实现。
主要用途包括:
-
提高程序执行效率:在多线程程序中,有时候某个线程的任务执行速度远远快于其他线程,此时使用
Thread.yield()
可以让其他线程有机会获得执行,提高整体程序的效率。 -
避免过度占用CPU:在某些情况下,线程可能会过度占用CPU资源,导致其他线程无法得到执行。使用
Thread.yield()
可以让线程主动让出CPU资源,避免过度占用。 -
调试和测试:在调试和测试多线程程序时,使用
Thread.yield()
可以模拟不同线程之间的执行顺序和时间片分配情况,帮助发现潜在的线程调度问题或性能瓶颈。
需要注意的是,Thread.yield()
方法只是对线程调度器的一种建议,并不保证其他线程一定会得到执行的机会,具体的线程调度行为还取决于操作系统和虚拟机的实现。因此,在使用Thread.yield()
时应该谨慎,并考虑到不同操作系统和虚拟机的行为差异。
16、yield 和 sleep 有什么区别?
yield
和sleep
是Java中两个用于线程控制的方法,它们有不同的用途和行为。下面是它们的主要区别:
-
基本功能:
yield
:yield
方法用于提示线程调度器当前线程愿意让出CPU执行时间片,允许其他具有相同优先级的线程运行。它是Thread
类的静态方法。sleep
:sleep
方法用于让当前线程休眠指定的时间,在这段时间内线程不会执行任何任务。它也是Thread
类的静态方法。
-
线程状态:
yield
:调用yield
方法后,当前线程从运行状态(Running)变为就绪状态(Runnable),然后重新参与线程调度。当前线程可能会立即被重新调度执行。sleep
:调用sleep
方法后,当前线程从运行状态变为阻塞状态(Blocked),在指定的时间过后,线程重新变为就绪状态(Runnable),等待CPU调度。
-
时间控制:
yield
:yield
方法不接受任何参数,因此它不能控制让出CPU的时间长度。它只是建议线程调度器重新调度。sleep
:sleep
方法接受一个以毫秒为单位的参数,可以精确控制线程休眠的时间长度。还有一个带有纳秒参数的重载版本,可以更精确地控制休眠时间。
-
释放锁:
yield
:yield
方法不会释放当前线程持有的任何锁。sleep
:sleep
方法也不会释放当前线程持有的任何锁。
-
使用场景:
yield
:yield
方法通常用于调试和测试,或在需要提示线程调度器当前线程可以让出CPU资源的情况下使用。它通常用于避免某些线程长时间占用CPU。sleep
:sleep
方法常用于模拟延迟、定时任务等需要线程暂停执行一段时间的场景。
下面是一个示例代码,演示了yield
和sleep
方法的使用:
在这个示例中,yieldThread
会在每次循环中调用yield
方法,提示线程调度器可以让出CPU,而sleepThread
会在每次循环中调用sleep
方法,使线程休眠1秒钟。
17、怎么理解 Java 中的线程中断?
在Java中,线程中断(interrupt)是一种协作机制,用于通知线程应当停止当前的工作或执行其他清理操作。中断机制不会强制性地终止线程,而是通过设置一个标志来提示线程自己响应中断信号。
线程中断的基本概念
- 中断标志:每个线程都有一个内部的中断标志,初始值为
false
。当一个线程调用另一个线程的interrupt()
方法时,目标线程的中断标志被设置为true
。 - 响应中断:线程需要自己检查中断标志,并决定如何响应。例如,可以选择停止任务、抛出异常、或者进行一些清理工作。
中断方法
-
interrupt()
:用于中断目标线程,设置其中断标志。 -
isInterrupted()
:检查线程的中断标志,但不清除中断状态。 -
interrupted()
:静态方法,检查当前线程的中断标志并清除中断状态。
中断的处理
线程应该定期检查自己的中断状态,并在适当的时候响应中断。例如,可以在执行循环中检查中断标志:
中断和阻塞方法
一些阻塞方法(如Thread.sleep()
、Object.wait()
、Thread.join()
等)在等待过程中,如果检测到中断,会抛出InterruptedException
异常并清除中断状态。此时,应该在捕获InterruptedException
后重新设置中断状态,以确保其他代码能够检测到中断。
总结
线程中断是Java中用于线程间协作的一种重要机制。它依赖于线程自身的响应,通过设置中断标志来提示线程停止当前任务或执行其他处理操作。正确理解和使用线程中断机制,有助于更好地控制多线程程序的执行流,确保在需要时能够平稳地停止或中断任务。
18、你怎么理解多线程分组?
多线程分组是Java中管理和组织线程的一种方式。线程分组(Thread Group)允许将多个线程归为一个组,并对这些线程进行统一管理。通过线程分组,可以更方便地管理和控制一组相关的线程,进行批量操作,如启动、停止、设置优先级等。
线程分组的基本概念
- ThreadGroup类:
ThreadGroup
是Java中用于线程分组的类。它提供了一系列方法,用于管理和操作线程组中的线程。
线程分组的优点
- 简化管理:将多个线程组织在一个组中,可以统一管理和控制,简化了多线程程序的设计和维护。
- 批量操作:可以对线程组中的所有线程进行批量操作,例如批量启动、批量中断等。
- 分层结构:线程组可以嵌套,形成分层结构。子线程组可以继承父线程组的某些属性,便于层次化管理。
创建和使用线程组
-
创建线程组:可以通过
ThreadGroup
的构造方法创建线程组。 -
将线程加入组:在线程创建时,可以指定所属的线程组。
-
操作线程组:可以使用
ThreadGroup
提供的方法对组内的线程进行操作。
示例代码
下面是一个简单的示例,演示了如何创建线程组并将线程加入组中进行管理:
在这个示例中,我们创建了一个名为"MyGroup"的线程组,并将两个线程加入该组。启动线程后,主线程休眠3秒钟,然后调用group.interrupt()
方法中断组内的所有线程。
总结
线程分组是Java中管理多线程的一种有效方式。通过将相关的线程组织在一个组中,可以简化线程管理和控制,进行批量操作,并支持分层结构的管理方式。正确理解和使用线程分组有助于编写高效、易维护的多线程程序。
19、你怎么理解 wait、notify、notifyAll?
在Java中,wait
、notify
和notifyAll
是用于线程间协作的三个重要方法。它们是Object
类的方法,这意味着每个Java对象都可以作为锁对象,并使用这些方法进行线程通信。这些方法通常与同步(synchronized
)块或方法一起使用,以确保线程在访问共享资源时的安全性和协调性。
wait
-
作用:使当前线程进入等待状态,直到被其他线程唤醒。调用
wait
方法的线程会释放它持有的对象锁,进入对象的等待队列中等待。 -
用法:
其中,
lock
是一个用于同步的对象。当线程调用wait
方法时,它会释放lock
对象的锁并进入等待状态,直到其他线程调用lock
对象的notify
或notifyAll
方法唤醒它。
notify
-
作用:唤醒在该对象的等待队列中等待的单个线程。如果有多个线程在等待,则选择其中一个线程唤醒。被唤醒的线程会尝试重新获取对象锁,获取到锁后才能继续执行。
-
用法:
notifyAll
-
作用:唤醒在该对象的等待队列中等待的所有线程。被唤醒的线程会尝试重新获取对象锁,获取到锁后才能继续执行。
-
用法:
示例代码
下面是一个示例,演示了wait
、notify
和notifyAll
的使用场景:
关键点总结
- 使用
wait
方法:当线程需要等待某个条件时,可以调用wait
方法进入等待状态,同时释放对象锁。 - 使用
notify
方法:当某个条件满足时,可以调用notify
方法唤醒一个等待中的线程,继续执行。 - 使用
notifyAll
方法:当某个条件满足时,可以调用notifyAll
方法唤醒所有等待中的线程,继续执行。 - 同步块中的使用:
wait
、notify
和notifyAll
方法必须在同步块或同步方法中调用,以确保当前线程持有对象的锁。 - 锁的释放和重新获取:调用
wait
方法时,线程会释放对象锁,进入等待状态。被notify
或notifyAll
唤醒后,线程会重新尝试获取对象锁,获取到锁后才能继续执行。
通过正确理解和使用wait
、notify
和notifyAll
方法,可以有效地实现线程间的协作和同步,确保多线程程序的正确性和效率。
20、同步和异步的区别?
同步和异步是两种处理任务的不同方式,主要区别在于任务的执行方式和对资源的等待情况。
同步
-
定义:同步意味着任务按顺序执行,一个任务必须等到前一个任务完成后才能开始。
-
特点:
- 阻塞:在执行同步任务时,调用者必须等待任务完成,无法继续执行其他任务。
- 顺序执行:任务按照调用的顺序依次执行,不会同时进行。
-
优点:
- 简单易理解,代码执行顺序清晰。
- 更容易处理数据一致性问题。
-
缺点:
- 效率低,调用者必须等待,资源利用率低。
-
示例:
异步
-
定义:异步意味着任务可以并发执行,一个任务不需要等到前一个任务完成后再开始,可以在等待其他任务的同时继续执行其他操作。
-
特点:
- 非阻塞:在执行异步任务时,调用者不必等待任务完成,可以继续执行其他任务。
- 并发执行:多个任务可以同时进行,提高资源利用率。
-
优点:
- 提高效率,调用者无需等待,可以同时处理多个任务。
- 更好的资源利用率,尤其在I/O操作时。
-
缺点:
- 代码复杂度高,需要处理并发问题。
- 数据一致性和同步问题需要额外注意。
-
示例:
同步和异步的区别
-
任务执行方式:
- 同步:任务按顺序执行,一个任务等待前一个任务完成。
- 异步:任务并发执行,一个任务不等待其他任务完成。
-
阻塞与非阻塞:
- 同步:调用者必须等待任务完成,阻塞执行。
- 异步:调用者无需等待任务完成,非阻塞执行。
-
效率:
- 同步:效率较低,等待时间长。
- 异步:效率高,充分利用资源。
-
适用场景:
- 同步:适用于简单、顺序执行的任务。
- 异步:适用于复杂、需要并发处理的任务,如网络请求、I/O操作等。
示例对比
以下是一个同步和异步读取文件的示例对比:
同步读取文件:
异步读取文件:
总结
- 同步:任务按顺序执行,阻塞调用者,适用于简单任务。
- 异步:任务并发执行,非阻塞调用者,适用于需要并发处理的复杂任务。
- 选择:根据任务的需求选择同步或异步方式,综合考虑代码复杂度、资源利用率和执行效率。
21、什么是死锁?
死锁是指两个或多个线程在运行过程中因争夺资源而造成的一种互相等待的现象。如果没有外部干预,这些线程将永远互相等待下去,无法继续执行。死锁是一种严重的并发问题,会导致程序无法正常运行。
死锁的四个必要条件
根据Coffman条件,发生死锁必须满足以下四个条件:
- 互斥条件:至少有一个资源是被独占使用的,即某个资源一次只能被一个线程占用。
- 持有并等待条件:一个线程已经持有至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占用,此时该线程阻塞,但它不释放自己已占有的资源。
- 不剥夺条件:线程已经获得的资源在未使用完之前不能被强行剥夺,只能在使用完毕后自行释放。
- 循环等待条件:存在一个线程等待链(比如T1等待T2,T2等待T3,...,Tn等待T1),使得链中的每个线程都在等待下一个线程所持有的资源,从而形成一个循环等待。
死锁示例
以下是一个简单的示例,演示了如何在Java中出现死锁的情况:
在上述示例中:
- 线程1首先获取
resource1
的锁,然后尝试获取resource2
的锁。 - 线程2首先获取
resource2
的锁,然后尝试获取resource1
的锁。 - 如果线程1在获取
resource2
的锁时阻塞,而线程2在获取resource1
的锁时阻塞,两个线程将永远相互等待,从而产生死锁。
避免死锁的方法
- 破坏互斥条件:尽量减少资源独占的情况,使用并发容器或其他无锁数据结构。
- 破坏持有并等待条件:在请求资源时,一次性请求所有资源,避免在持有资源的同时请求其他资源。
- 破坏不剥夺条件:允许资源被剥夺,使用可中断的锁(如
ReentrantLock
的lockInterruptibly
方法)。 - 破坏循环等待条件:按一定顺序获取资源,确保所有线程按照相同顺序请求资源,避免循环等待。
示例:破坏循环等待条件
通过对资源进行排序,并按照顺序请求资源,可以有效避免死锁。以下是一个示例:
在这个示例中,两个线程都按相同顺序获取资源,从而避免了循环等待的发生。
总结
死锁是指多个线程互相等待对方持有的资源而无法继续执行的一种状态。了解和避免死锁对于编写健壮的多线程程序至关重要。通过破坏死锁的必要条件,可以有效地避免死锁的发生,提高程序的稳定性和可靠性。
22、怎么避免死锁?
避免死锁是多线程编程中的一个重要课题。以下是一些常见的策略和方法来预防和避免死锁:
1. 破坏互斥条件
- 无锁并发数据结构:使用无锁并发数据结构,如
java.util.concurrent
包中的并发集合。 - 细粒度锁:使用更细粒度的锁来减少锁的范围和持有时间。
2. 破坏持有并等待条件
-
一次性获取所有资源:在请求资源时,尝试一次性请求所有需要的资源。如果无法一次性获取所有资源,则释放已持有的资源并重新尝试获取。
3. 破坏不剥夺条件
-
使用可中断的锁:使用
java.util.concurrent.locks.ReentrantLock
的lockInterruptibly
方法,允许在等待锁时被中断。
4. 破坏循环等待条件
-
对资源进行排序:为所有资源分配一个全局顺序,所有线程按照这个顺序请求资源,从而避免循环等待。
5. 超时机制
-
使用带超时的锁:使用
tryLock
方法尝试获取锁,并设置超时时间。若在指定时间内无法获取锁,则放弃获取锁,以避免死锁。
6. 监控和检测
-
死锁检测:使用监控工具(如Java VisualVM)或程序代码进行死锁检测,定期检查系统是否存在死锁,并采取措施恢复。
7. 避免嵌套锁
- 避免嵌套锁定:尽量避免在一个同步块中再进入另一个同步块,这样可以减少发生死锁的可能性。
总结
避免死锁的关键在于预防和检测。通过破坏死锁的必要条件、合理设计锁的使用策略、使用带超时的锁机制以及定期进行死锁检测,可以有效地避免和处理死锁问题。合理的代码设计和规范的资源管理是确保多线程程序稳定运行的重要保障。
23、什么是活锁?
活锁是一种并发问题,类似于死锁,但不同之处在于线程或进程并没有被完全阻塞。相反,它们仍在不断地更改状态,但由于某种原因无法取得进展。这通常是因为线程在不断地重复执行一些操作,并且每次执行都因为某种条件未满足而无法前进,从而陷入一个无限循环中。
活锁的特点
- 线程未被阻塞:线程仍在运行并尝试完成任务。
- 无法取得进展:尽管线程在运行,但它们在某种条件下不断改变状态,无法完成实际工作。
- 状态不断变化:线程的状态不断变化,但由于条件或竞争,无法前进。
活锁的示例
以下是一个活锁的简单示例,其中两个线程不断地试图改变彼此的状态,但由于彼此干扰,始终无法完成任务:
在这个示例中,两个线程(thread1和thread2)尝试锁定两个资源(resource1和resource2)。每个线程在获取到一个资源锁后,如果发现另一个资源已被锁定,则会释放已持有的资源锁并重试。这种情况下,两个线程可能会无限循环,导致活锁。
避免活锁的方法
-
引入随机性:在重试操作之间引入一些随机延迟,减少线程之间的竞争概率。
-
减少竞争:设计更有效的资源管理和调度策略,避免频繁的资源争夺。
-
限制重试次数:设置一个最大重试次数,如果超过该次数,则采取其他措施,如记录日志或通知用户。
-
使用高级并发工具:使用
java.util.concurrent
包中的高级并发工具和算法,如Semaphore
、CountDownLatch
、CyclicBarrier
等,来控制线程的协调和调度。
总结
活锁是一种线程或进程不断改变状态但无法取得进展的并发问题。避免活锁需要合理的设计资源管理和调度策略,引入随机性或限制重试次数等方法来减少线程之间的竞争。通过使用更高级的并发工具和算法,可以有效地避免和处理活锁问题,确保多线程程序的稳定性和效率。
24、什么是无锁?
无锁编程是一种并发编程技术,旨在在多线程环境中实现数据共享和操作,而不使用传统的锁机制(如Mutex
、synchronized
块等)。无锁编程通过使用原子操作、内存屏障和其他并发控制技术来避免因锁竞争导致的性能瓶颈和死锁问题。
无锁编程的特点
- 高性能:无锁编程减少了线程之间的上下文切换和锁争夺,提高了并发性能。
- 避免死锁:由于不使用传统的锁机制,因此避免了死锁问题。
- 复杂性:无锁编程通常比有锁编程更复杂,需要仔细设计和验证正确性。
常见的无锁数据结构
- 无锁队列:如Michael-Scott队列(Java中的
ConcurrentLinkedQueue
)。 - 无锁栈:如Treiber栈(Java中的
ConcurrentLinkedDeque
)。 - 无锁列表:如Harris链表。
实现无锁编程的基础
- 原子操作:原子操作是不可分割的操作,即在执行过程中不会被其他线程中断。常见的原子操作包括比较并交换(Compare-And-Swap,CAS)和获取并增加(Fetch-And-Add)。
- 内存屏障:内存屏障(Memory Barrier)是一种同步机制,用于确保某些内存操作的顺序。它可以防止编译器和处理器对内存操作进行重排序。
- volatile关键字:在Java中,
volatile
关键字用于声明变量,使得对该变量的读写操作都是直接从主内存中进行的,而不是从线程的本地缓存中读取,从而保证了可见性。
CAS操作
CAS操作是无锁编程的核心,它通过比较内存中的值和预期值,如果相等则更新为新值。CAS操作在硬件级别上是原子的。Java中的java.util.concurrent.atomic
包提供了CAS操作的支持。
示例:使用CAS实现无锁计数器
在这个示例中,increment
方法使用CAS操作来实现无锁递增。compareAndSet
方法比较当前值和期望值,如果相等则更新为新值,否则继续重试,直到成功。
无锁编程的挑战
- 正确性验证:无锁算法需要仔细验证其正确性,确保不会出现数据竞争和一致性问题。
- ABA问题:CAS操作中的ABA问题指的是一个值在比较过程中被修改为另一个值,然后又被修改回原值,导致CAS操作误以为值未被修改。可以通过使用版本号或标记来解决ABA问题。
- 可扩展性:无锁算法在高并发环境下可能会遇到扩展性问题,需要设计良好的数据结构和算法。
总结
无锁编程通过使用原子操作和内存屏障来避免传统锁机制的性能瓶颈和死锁问题。尽管无锁编程提高了并发性能,但也带来了更高的复杂性和验证正确性的挑战。通过使用CAS操作和合理设计数据结构,可以实现高效的无锁并发程序。
25、什么是线程饥饿?
线程饥饿是指在多线程环境中,某些线程长期得不到执行的机会,因为系统资源(如CPU时间片、锁等)被其他线程持续占用,导致这些线程无法完成任务。线程饥饿会影响程序的性能和响应性。
线程饥饿的原因
- 不公平的锁机制:使用非公平锁时,线程获取锁的顺序是不确定的,可能导致某些线程长时间得不到锁。
- 高优先级线程占用资源:高优先级线程持续占用资源,低优先级线程一直无法获取执行机会。
- 资源竞争:多个线程争夺有限的资源,如数据库连接、文件句柄等,某些线程总是被其他线程抢先获取资源。
线程饥饿的示例
以下是一个简单的示例,演示了不公平锁导致的线程饥饿问题:
在这个示例中,多个线程使用一个非公平锁进行竞争,可能会导致某些线程长期无法获取锁,导致饥饿。
避免线程饥饿的方法
-
使用公平锁:公平锁保证了线程获取锁的顺序是按照请求的顺序,避免某些线程长期得不到锁。
-
线程优先级:合理设置线程的优先级,避免高优先级线程长期占用资源。
-
避免长时间占用资源:确保线程在获取锁后尽快释放锁,避免长时间占用资源。
-
资源池化:使用资源池(如线程池、数据库连接池等),合理管理资源分配,避免资源竞争导致的饥饿。
-
监控和调整:通过监控系统性能,及时发现和调整线程饥饿问题,优化系统资源分配。
总结
线程饥饿是指某些线程长期得不到执行机会的问题。通过使用公平锁、合理设置线程优先级、避免长时间占用资源、使用资源池化技术和监控系统性能,可以有效避免和解决线程饥饿问题,从而提高系统的并发性能和响应性。
26、什么是 CAS?
CAS是"Compare And Swap"(比较并交换)的缩写,是一种无锁算法,用于实现多线程环境下的并发控制。CAS操作是原子操作的一种,它包括三个步骤:比较、写入和返回结果。
CAS操作的基本原理是,它会比较内存中的某个值与预期值是否相等,如果相等,则将新值写入内存中;如果不相等,则说明其他线程已经修改了内存中的值,CAS操作会失败并返回失败标志。
CAS操作的三个基本步骤:
- 比较(Compare):首先读取内存中的值与预期值进行比较。
- 写入(And):如果比较相等,则将新值写入内存中。
- 返回结果(Swap):无论操作成功与否,CAS操作都会返回操作前的值。
CAS的语义:
- 如果当前值等于预期值,则以新值更新当前值,并返回成功;
- 如果当前值不等于预期值,则不更新当前值,并返回失败。
CAS的优点:
- 无锁操作:CAS是一种无锁算法,避免了传统锁机制的性能开销和死锁问题。
- 原子性:CAS操作是原子操作,可以保证多线程环境下的并发安全性。
- 高性能:由于避免了锁竞争,CAS操作具有较高的性能。
CAS的缺点:
- ABA问题:CAS操作可能存在ABA问题,即在比较和写入之间,内存中的值可能被改变为其他值,然后再改回原来的值,导致CAS操作误以为值未被修改。解决ABA问题可以使用版本号或标记来避免。
- 循环开销:CAS操作通常需要通过循环重试来保证操作的原子性,如果重试次数过多,会增加CPU的开销。
示例:使用CAS实现原子计数器
在这个示例中,AtomicInteger
类使用CAS操作来实现原子的增加和减少操作,保证了多线程环境下的并发安全性和性能。
27、阻塞和非阻塞的区别?
阻塞和非阻塞是指在系统中等待资源或执行任务时的两种不同方式。
- 阻塞:
- 定义:在阻塞模式下,当一个线程请求资源或执行任务时,如果资源不可用或任务无法立即完成,线程将被挂起(暂停执行),直到资源可用或任务完成后才继续执行。
- 特点:阻塞模式下,线程会等待直到获取所需资源或任务完成,期间不会执行其他操作。
- 示例:典型的阻塞操作包括使用
Object.wait()
、Thread.sleep()
、I/O操作中的阻塞式调用等。
- 非阻塞:
- 定义:在非阻塞模式下,当一个线程请求资源或执行任务时,如果资源不可用或任务无法立即完成,线程不会被挂起,而是会立即返回一个状态或错误信息,让线程可以继续执行其他操作。
- 特点:非阻塞模式下,线程不会等待资源或任务完成,而是立即返回,让线程可以继续执行其他操作,或者通过轮询等方式检查资源是否可用。
- 示例:典型的非阻塞操作包括使用
Thread.yield()
、使用select()
或poll()
等轮询式的 I/O 操作。
阻塞和非阻塞的比较:
- 响应性:非阻塞模式下,线程可以立即返回并继续执行其他操作,因此具有更好的响应性,可以减少系统等待时间。而阻塞模式下,线程需要等待资源可用或任务完成后才能继续执行,响应性相对较差。
- 资源利用:非阻塞模式下,线程可以利用等待资源的时间执行其他任务,提高资源利用率。而阻塞模式下,线程被挂起时无法执行其他任务,资源利用率较低。
- 编程复杂性:非阻塞模式下,由于需要手动检查资源状态或轮询等,编程复杂性较高。而阻塞模式下,线程等待资源时不需要额外处理,编程相对简单。
在实际开发中,通常会根据需求和场景选择合适的阻塞或非阻塞方式。例如,在高并发环境下,非阻塞模式可以提高系统的并发性能和响应性;而在某些需要等待资源可用的情况下,阻塞模式则更为合适。
28、并发和并行的区别?
并发和并行是指多个任务或操作在同一时间段内执行的两种不同方式。
- 并发:
- 定义:并发是指多个任务在同一时间段内交替进行,每个任务可能只执行一部分,然后切换到下一个任务。在并发模式下,多个任务之间通过时间片轮转或事件驱动等方式交替执行,看起来好像同时执行。
- 特点:并发模式下,多个任务共享系统资源,任务之间可能会发生竞争和调度,但实际上在同一时刻只有一个任务在执行。
- 示例:典型的并发应用包括多线程程序、网络服务器处理多个请求、操作系统中的进程和线程管理等。
- 并行:
- 定义:并行是指多个任务在同一时刻真正同时执行,每个任务独立占用系统的不同资源(如不同的CPU核心)。在并行模式下,多个任务可以同时运行,互不干扰。
- 特点:并行模式下,多个任务同时执行,可以充分利用系统的多核心或多处理器资源,提高整体性能。
- 示例:典型的并行应用包括多线程并发执行、分布式系统中的分布式计算、图形处理单元(GPU)等。
并发和并行的比较:
- 执行方式:并发是交替执行多个任务,看起来好像同时执行,但实际上在同一时刻只有一个任务在执行;而并行是真正同时执行多个任务,每个任务独立占用系统资源。
- 资源利用:并发模式下,多个任务共享系统资源,可能会发生竞争和调度,资源利用率较低;而并行模式下,每个任务独立占用系统资源,可以充分利用多核心或多处理器资源,资源利用率较高。
- 性能提升:并行模式下,多个任务可以同时执行,可以提高整体性能;而并发模式下,由于任务交替执行,性能提升相对较低。
在实际应用中,可以根据任务的性质和系统资源来选择合适的并发或并行方式。例如,对于需要高吞吐量的计算任务,可以采用并行计算;对于IO密集型的任务,可以采用并发模式来提高系统响应性。
29、为什么不推荐使用 stop 停止线程?
不推荐使用 stop
方法来停止线程的主要原因是因为这个方法可能导致线程处于不一致的状态,引发各种潜在的问题,包括数据损坏、资源泄漏、死锁等。下面是具体的原因:
-
数据一致性问题:线程在执行过程中可能会修改共享数据,如果突然停止线程,可能导致数据处于不一致的状态,造成数据损坏。
-
资源泄漏:如果线程被停止时正在占用一些资源,如锁、文件句柄、数据库连接等,突然停止线程可能导致这些资源无法正确释放,从而导致资源泄漏。
-
死锁问题:如果线程在执行过程中持有锁,并且被突然停止,那么其他线程可能无法获取到这个锁,导致死锁问题。
-
线程没有机会做清理工作:线程的停止过程中,一般需要执行一些清理工作,如释放资源、关闭连接等。使用
stop
方法无法保证线程有机会执行这些清理工作。 -
不安全:
stop
方法是一个不安全的方法,因为它直接终止线程而不考虑线程当前的状态和执行过程中的上下文,容易引发各种问题。
推荐的替代方案:
-
使用标识符控制线程:在线程的执行过程中,通过设置一个标识符来控制线程的执行状态,当需要停止线程时,设置标识符为停止状态,并在适当的时候退出线程执行。
-
优雅的停止:在线程的执行过程中,可以通过检查中断标志位或其他状态来判断是否需要停止线程,然后在合适的时机优雅地退出线程执行。
-
使用
interrupt
方法:可以使用interrupt
方法来中断线程,线程在被中断时可以进行相应的处理,例如捕获中断异常并做清理工作后退出线程。
这些替代方案相对安全,能够更好地控制线程的停止过程,避免了突然终止线程可能引发的各种问题。
30、如何优雅地终止一个线程?
优雅地终止一个线程通常包括以下几个步骤:
-
使用标识符控制线程状态:在线程的执行过程中,使用一个标识符来表示线程的状态,例如一个布尔变量或枚举类型。这个标识符可以用来控制线程的执行和停止。
-
检查中断状态:在线程的执行过程中,可以使用
Thread.interrupted()
或Thread.currentThread().isInterrupted()
方法来检查线程的中断状态。如果线程被中断,可以根据需要进行相应的处理。 -
优雅地退出循环:在线程的执行逻辑中,使用循环来执行任务。在每次循环迭代开始时,检查线程的标识符状态或中断状态,如果需要停止线程,则优雅地退出循环,完成清理工作后退出线程执行。
-
捕获中断异常:在执行过程中,可能会涉及到需要捕获中断异常的代码块,例如使用
Thread.sleep()
、Object.wait()
等会抛出中断异常的方法。在捕获中断异常后,可以根据需要进行相应的处理,例如完成清理工作后退出线程执行。
下面是一个示例代码,演示了如何优雅地终止一个线程:
在这个示例中,线程在执行过程中通过检查标识符状态 running
和中断状态来决定是否退出循环并停止线程,实现了优雅地终止线程的过程。
31、Synchronized 同步锁有哪几种用法?
synchronized
同步锁可以用于不同的场景和方式,主要包括以下几种用法:
- 同步实例方法:
在实例方法上添加 synchronized
关键字,使得该方法成为同步方法。当一个线程调用这个同步方法时,会获取该实例对象的锁,其他线程需要等待锁释放后才能执行该方法。
- 同步静态方法:
在静态方法上添加 synchronized
关键字,使得该静态方法成为同步静态方法。同步静态方法的锁是类对象,所有实例共享这个锁,因此同一时间只有一个线程可以执行该静态方法。
- 同步代码块(对象锁):
使用同步代码块可以指定锁定的对象,这里使用的是当前实例对象 this
。当一个线程进入同步代码块时,会获取指定对象的锁,其他线程需要等待锁释放后才能执行同步代码块。
- 同步代码块(类锁):
使用同步代码块可以指定类对象作为锁,这里使用的是类名 ClassName.class
。同步类锁的效果和同步静态方法类似,锁定的是整个类对象,因此同一时间只有一个线程可以执行同步代码块。
- 同步方法和同步块的区别:
- 同步方法的锁是当前对象实例(或者类对象,对于静态方法),而同步代码块可以指定锁定的对象。
- 同步方法会锁住整个方法体,而同步代码块可以精确控制需要同步的代码段,提高程序的并发性能。
- 同步方法适用于对整个方法进行同步控制的场景,而同步代码块适用于对部分代码段进行同步控制的场景。
选择合适的同步锁用法取决于具体的业务需求和并发控制的粒度。
32、什么是重入锁(ReentrantLock)?
重入锁(ReentrantLock)是 Java 中提供的一种同步机制,它可以实现类似于 synchronized 关键字的同步功能,但相比 synchronized 关键字更加灵活和强大。
重入锁的特点包括:
-
可重入性:重入锁支持线程的重复进入,即一个线程可以多次获得同一把锁,而不会造成死锁或其他问题。
-
公平性:重入锁可以实现公平性,即按照线程请求锁的顺序来获取锁,避免某些线程一直无法获取锁的情况。
-
条件等待:重入锁提供了条件(Condition)等待机制,可以实现更复杂的线程同步和通信。
-
超时等待:重入锁支持指定超时时间,在尝试获取锁时可以设定等待的最大时间,避免线程长时间等待。
重入锁与 synchronized 关键字相比具有更灵活的特性,但使用时需要注意以下几点:
-
需要手动释放锁:在使用 synchronized 关键字时,锁的获取和释放是隐式的,而在使用重入锁时需要手动调用
lock()
方法获取锁,并在适当的时候调用unlock()
方法释放锁,否则可能导致死锁或其他问题。 -
灵活性和复杂性:重入锁提供了更多灵活性和功能,但也增加了代码的复杂性,需要开发人员根据具体需求选择合适的同步机制。
-
性能优化:在某些情况下,重入锁的性能可能优于 synchronized 关键字,但在大部分情况下,两者的性能差异并不明显,因此在选择同步机制时应根据实际场景进行评估和选择。
总之,重入锁是一种强大的同步机制,在需要更灵活的同步控制和条件等待时可以考虑使用它。
33、Synchronized 与 ReentrantLock 的区别?
Synchronized
关键字和 ReentrantLock
都是 Java 中用于实现线程同步的机制,它们之间有以下几点区别:
- 可重入性:
Synchronized
关键字是可重入的,一个线程可以多次获取同一把锁,而不会发生死锁。ReentrantLock
也是可重入的,支持同一线程多次获取锁,不会导致死锁。
- 锁的获取方式:
Synchronized
关键字是隐式获取锁的,在进入同步代码块或同步方法时自动获取锁,在退出同步代码块或方法时自动释放锁。ReentrantLock
是显式获取锁的,需要通过调用lock()
方法来获取锁,通过调用unlock()
方法来释放锁。
- 公平性:
Synchronized
关键字不具备公平性,无法保证线程按照请求锁的顺序获取锁。ReentrantLock
可以通过构造函数指定是否具有公平性,如果构造时传入true
,则是公平锁,按照线程请求锁的顺序获取锁。
- 性能:
- 在低竞争情况下,
Synchronized
关键字的性能通常优于ReentrantLock
,因为Synchronized
关键字是 JVM 内置的机制,无需额外的操作。 - 在高竞争情况下,
ReentrantLock
的性能可能优于Synchronized
,因为它提供了更多的灵活性和功能,如可中断锁、定时锁等。
- 条件等待:
Synchronized
关键字支持简单的条件等待和通知机制,通过wait()
、notify()
和notifyAll()
方法实现。ReentrantLock
提供了更强大的条件等待和通知机制,通过Condition
接口的await()
、signal()
和signalAll()
方法实现。
综上所述,Synchronized
关键字是更简单、更常用的同步机制,适用于大部分情况下;而 ReentrantLock
提供了更多灵活性和功能,适用于需要更复杂同步控制、条件等待和公平性要求较高的场景。在选择使用时,应根据具体需求和性能考量进行选择。
34、synchronized 锁的是什么?
synchronized
关键字可以用于实现线程同步,它锁的是对象或类的实例。具体来说,它可以锁定以下几种范围:
- 同步实例方法:当
synchronized
关键字用于实例方法时,它锁的是当前对象实例(即方法所属的对象),只有获取了该对象实例的锁才能执行同步方法。不同的实例拥有不同的锁,因此不同实例的同步方法之间互不影响。
- 同步代码块(对象锁):当
synchronized
关键字用于代码块时,可以指定锁定的对象,通常使用this
表示当前对象实例,也可以是其他对象。
在这种情况下,锁的范围是指定的对象,不同的对象拥有不同的锁,因此不同对象之间的同步代码块互不影响。
- 同步静态方法:当
synchronized
关键字用于静态方法时,它锁的是类对象,即整个类的实例共享同一把锁,不同的实例间共享同一个锁。
因此,在使用 synchronized
关键字时,需要根据具体情况选择合适的锁定范围,以确保线程同步的正确性和效率。
35、什么是读写锁?
读写锁(ReadWriteLock)是一种并发控制机制,它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁的设计是为了提高读取操作的并发性能,在读操作远远多于写操作的场景下,可以有效减少写操作对整体性能的影响。
读写锁的特点包括:
-
共享资源的读取:多个线程可以同时获取读锁进行读取操作,不会互斥,从而提高了读取操作的并发性能。
-
排他性写入:写锁是排他的,只有获取了写锁的线程可以进行写入操作,其他线程无法同时获取读或写锁。
-
读写互斥:写锁和读锁之间是互斥的,即当一个线程持有写锁时,其他线程无法同时获取读或写锁,直到写锁释放。
-
可降级:读写锁支持从写锁降级为读锁,但不支持从读锁升级为写锁。
Java 中的读写锁接口是 java.util.concurrent.locks.ReadWriteLock
,常用的实现类是 ReentrantReadWriteLock
。它包含两个锁:读锁(ReadLock)和写锁(WriteLock),通过这两个锁来实现读写操作的并发控制。
使用读写锁的场景通常是读操作频繁,而写操作较少的情况下,通过读写锁可以提高系统的并发性能。需要注意的是,在使用读写锁时要避免写锁长时间占用,以免影响读取操作的性能。
36、公平锁和非公平锁的区别?
公平锁和非公平锁的区别
公平锁和非公平锁是两种不同的锁获取策略,它们的主要区别在于线程获取锁的顺序和调度方式。
公平锁
- FIFO顺序:公平锁按照线程请求锁的先后顺序来分配锁,先请求锁的线程优先获取锁,后请求的线程需要排队等待。
- 避免饥饿:采用FIFO顺序,确保每个线程都能公平地获得执行机会,避免线程饥饿的问题。
- 性能较低:公平锁需要维护一个队列来管理线程请求,可能导致性能开销较大,尤其在高并发情况下。
非公平锁
- 无序竞争:非公平锁不按照请求顺序竞争锁,线程可以在任意时刻尝试获取锁,可能出现后请求的线程先获取锁的情况。
- 可能导致饥饿:由于没有严格的顺序,某些线程可能长时间得不到锁,出现线程饥饿的问题。
- 性能较高:非公平锁不需要维护线程请求队列,性能开销较小,在高并发情况下,通常比公平锁具有更好的性能表现。
示例:ReentrantLock
在 Java 中,ReentrantLock
提供了公平锁和非公平锁的实现。默认情况下,ReentrantLock
是非公平锁,可以通过构造函数来指定锁的公平性:
选择公平锁还是非公平锁
- 公平锁:适用于需要严格控制线程执行顺序,避免线程饥饿的场景,例如某些实时系统或关键资源访问控制。
- 非公平锁:适用于对性能要求较高,线程间竞争激烈的场景,在大多数情况下,非公平锁能够提供更高的吞吐量和更好的性能。
根据具体需求和性能要求选择合适的锁策略,以确保系统的正确性和高效运行。
37、有哪些锁优化的方式?
锁优化的方式
锁优化是提高并发程序性能的重要手段,主要目标是减少锁的争用和提升线程的执行效率。以下是几种常见的锁优化方式:
1. 减小锁的粒度
将大锁拆分为多个小锁,减少每个锁的竞争。例如,可以将一个大同步块拆分为多个小的同步块,或者将一个大的同步方法拆分为多个小的同步方法。
2. 使用读写锁(ReadWriteLock)
对于读操作远多于写操作的场景,可以使用读写锁来提高并发性能。读写锁允许多个读线程并发执行,但写线程是排他的。
3. 使用乐观锁
乐观锁假设对资源的访问大多数情况下是没有冲突的,因此不加锁而直接进行操作,如果操作期间检测到冲突,则重试操作。典型的实现是CAS(Compare-And-Swap)操作。
4. 锁分离
将不同的资源使用不同的锁,避免不必要的锁竞争。例如,使用不同的锁来保护读缓存和写缓存。
5. 使用无锁数据结构
无锁数据结构通过CAS等原子操作实现并发访问,避免使用传统的锁机制。例如,Java中的 ConcurrentHashMap
使用了无锁技术来实现高效的并发访问。
6. 减少锁的持有时间
尽量减少锁的持有时间,避免在持有锁期间执行耗时的操作。例如,将耗时的操作移到同步代码块之外执行。
通过以上锁优化方式,可以显著提高并发程序的性能,减少锁竞争带来的开销和等待时间。具体的优化策略应根据实际应用场景和性能瓶颈进行选择和调整。
38、什么是偏向锁?
偏向锁(Biased Locking)是 Java 虚拟机(JVM)中的一种锁优化机制,它通过消除不必要的同步开销来提高程序性能。偏向锁的设计思想是,在大多数情况下,锁通常是由同一个线程多次获得的,因此可以将锁偏向于首次获得它的线程,从而减少加锁和解锁的开销。
偏向锁的特点
- 偏向性:当一个线程第一次获得偏向锁时,锁会偏向于这个线程,后续的加锁和解锁操作都不需要进行同步操作。
- 轻量级:如果偏向锁没有竞争(即没有其他线程尝试获取这个锁),则不会进行CAS(Compare-And-Swap)操作,锁的获取和释放非常快速。
- 撤销偏向:如果有其他线程尝试获取已经偏向的锁,JVM 会撤销偏向锁,并根据情况升级为轻量级锁或重量级锁。
偏向锁的工作机制
- 获取偏向锁:当一个线程第一次获取锁时,JVM 会在对象头中记录偏向线程的ID,表示该锁偏向于这个线程。
- 再次获取锁:如果偏向线程再次获取这个锁,不需要进行任何同步操作,直接执行同步代码块。
- 撤销偏向锁:如果有其他线程尝试获取已经偏向的锁,JVM 会暂停偏向线程,检查锁的状态并撤销偏向锁。然后,锁会根据竞争情况升级为轻量级锁或重量级锁。
偏向锁的优缺点
-
优点:
- 提高性能:在无锁竞争的情况下,偏向锁避免了频繁的CAS操作,极大地提高了锁的获取和释放性能。
- 低开销:偏向锁的加锁和解锁操作非常轻量级,适合用于短时间持有锁的场景。
-
缺点:
- 撤销成本:如果偏向锁需要撤销(例如,有其他线程尝试获取锁),会带来一定的开销,包括暂停线程和升级锁的操作。
- 场景有限:偏向锁适用于无锁竞争的场景,如果锁竞争激烈,偏向锁反而会增加额外的开销。
示例
默认情况下,偏向锁是启用的,但可以通过 JVM 参数来禁用它:
偏向锁在某些特定场景下能够显著提高性能,但在高竞争环境下可能效果不佳。因此,在实际应用中,应根据具体的工作负载和性能需求,适当地选择和调整锁的优化策略。
39、什么是轻量级锁?
轻量级锁(Lightweight Locking)是 Java 虚拟机(JVM)中的一种锁优化机制,旨在减少传统重量级锁(依赖操作系统的互斥量或监视器)带来的性能开销。轻量级锁通过使用CAS(Compare-And-Swap)操作来实现线程间的同步,提高了在无锁竞争或轻度竞争场景下的性能。
轻量级锁的工作原理
-
加锁:
- 当一个线程进入同步块时,如果该对象的锁是未锁定状态,JVM 会首先在当前线程的栈帧中创建一个锁记录(Lock Record),然后将对象头中的Mark Word复制到这个锁记录中。
- 然后,JVM 会尝试使用CAS操作将对象头中的Mark Word更新为指向锁记录的指针。如果CAS操作成功,表示该线程成功获取了轻量级锁。
- 如果CAS操作失败,表示锁已经被其他线程持有,JVM 会尝试进行自旋(忙等待)操作。如果自旋失败或者锁竞争严重,则会膨胀为重量级锁。
-
解锁:
- 当持有轻量级锁的线程退出同步块时,JVM 会使用CAS操作尝试将对象头中的Mark Word恢复为原来的锁记录。如果恢复成功,表示解锁成功。
- 如果恢复失败,表示有其他线程在尝试获取锁,锁会膨胀为重量级锁。
轻量级锁的优缺点
-
优点:
- 提高性能:在无锁竞争或轻度竞争场景下,轻量级锁避免了重量级锁的上下文切换和线程阻塞,显著提高了性能。
- 低开销:轻量级锁使用CAS操作进行同步,开销较低,适用于短时间持有锁的场景。
-
缺点:
- 自旋等待开销:如果锁竞争激烈,自旋操作会消耗CPU资源,可能导致性能下降。
- 膨胀为重量级锁:在锁竞争严重的情况下,轻量级锁会膨胀为重量级锁,带来额外的开销。
示例
轻量级锁是 JVM 自动进行优化的,无需开发者显式编码。JVM 参数可以用来启用或禁用轻量级锁:
使用场景
轻量级锁适用于锁竞争较少的场景,例如大多数情况下只有一个线程获取锁,或多个线程轮流获取锁的场景。它通过减少重量级锁的开销,提高了同步代码块的执行效率。
总结
轻量级锁是 JVM 提供的一种锁优化机制,主要通过CAS操作和自旋等待实现线程同步。在无锁竞争或轻度竞争的情况下,轻量级锁能够显著提高性能,但在高竞争环境下可能效果不佳,锁会膨胀为重量级锁。根据具体的应用场景和性能需求,适当地利用轻量级锁可以有效提高并发程序的性能。
40、什么是自旋锁?
自旋锁(Spin Lock)是一种用于多线程同步的锁机制,它通过忙等待(自旋)来尝试获取锁,而不是将线程挂起。这种锁的主要特点是当一个线程尝试获取锁但未成功时,它会在循环中反复检查锁的状态,直到获取到锁为止。
自旋锁的工作原理
- 获取锁:当一个线程尝试获取自旋锁时,如果锁已经被其他线程持有,该线程不会被挂起,而是进入一个循环,反复检查锁的状态。
- 忙等待:在循环过程中,线程会不断检查锁是否被释放,如果锁被释放,线程会尝试重新获取锁。
- 成功获取锁:一旦锁被释放且线程成功获取锁,线程即可进入临界区执行相关操作。
自旋锁的优缺点
-
优点:
- 低延迟:自旋锁避免了线程挂起和恢复的开销,因此在锁持有时间很短的情况下,使用自旋锁可以减少线程上下文切换,提高性能。
- 简单实现:自旋锁的实现相对简单,不需要操作系统内核的支持,适用于用户态的锁机制。
-
缺点:
- 高CPU开销:自旋锁在等待期间会占用CPU资源进行忙等待,导致高CPU开销,特别是在锁竞争激烈或锁持有时间较长的情况下。
- 不可重入:自旋锁通常是不可重入的,即同一个线程不能多次获取同一个自旋锁,否则会导致死锁。
自旋锁的实现
在 Java 中,可以使用 java.util.concurrent.atomic
包中的原子操作来实现自旋锁。例如,使用 AtomicBoolean
来实现一个简单的自旋锁:
在这个示例中,compareAndSet
方法用于尝试获取锁,如果锁已经被其他线程持有(即 lock
为 true
),当前线程会继续自旋等待,直到成功获取锁。
自旋锁的使用场景
自旋锁适用于以下场景:
- 锁持有时间很短:如果临界区的代码执行时间很短,自旋锁可以避免线程挂起和恢复的开销,提高性能。
- 多核处理器:在多核处理器上,自旋锁的忙等待可以在其他核心上运行的线程释放锁后迅速获取锁,提高并发性能。
- 轻度竞争:在锁竞争不激烈的情况下,自旋锁可以高效地处理线程同步。
不适用的场景
自旋锁不适用于锁持有时间较长或锁竞争激烈的场景,因为忙等待会导致高CPU开销,降低系统的整体性能。在这种情况下,传统的重量级锁或其他同步机制可能更合适。
总结
自旋锁是一种通过忙等待实现线程同步的锁机制,适用于锁持有时间短和锁竞争不激烈的场景。它通过避免线程挂起和恢复的开销,提高了同步操作的性能,但也带来了高CPU开销的问题。因此,在实际应用中,需要根据具体的需求和性能要求选择合适的锁机制。
41、什么是锁消除?
锁消除 是 Java 虚拟机在进行即时编译(Just-In-Time Compilation, JIT)时的一种优化技术。它的目的是在编译时分析代码,自动移除不必要的同步锁,从而提高程序的执行效率。
当编译器确定某些锁对象不会被多线程访问时,就会进行锁消除。例如,在某些方法内部,局部变量的锁定在单线程环境下是没有意义的,因为局部变量只能被当前线程访问,不可能引起线程竞争。
锁消除的实现原理:
在 JIT 编译过程中,编译器会对同步块进行逃逸分析(Escape Analysis),确定对象是否会逃逸出线程的作用范围。如果对象不会逃逸(即只在线程内部使用),编译器就会消除该对象的锁。
逃逸分析:
逃逸分析是一种动态分析技术,用来确定对象的引用范围。如果编译器通过逃逸分析发现对象没有逃逸到当前线程之外,则可以进行锁消除。
示例:
在上面的代码中,StringBuilder
对象 sb
作为方法参数传递,而且这个方法不会将 sb
引用保存到其他地方,因此它不会逃逸出调用线程的范围。JIT 编译器在进行逃逸分析时会发现这一点,并消除掉 StringBuilder
的内部锁定操作。
在这种情况下,StringBuilder
的 append
方法会被多次调用,但由于 sb
对象不会逃逸出 testLockElimination
方法,因此 JIT 编译器可以安全地消除掉 StringBuilder
内部的同步锁,从而提升性能。
锁消除的好处:
- 提升性能:减少了不必要的锁开销,降低了同步带来的性能损耗。
- 简化代码:程序员无需手动优化锁机制,编译器会自动进行优化。
总结:
锁消除是 JIT 编译器在进行即时编译时,通过逃逸分析确定锁对象不会被多线程访问,从而自动移除不必要的同步锁的优化技术。这种优化提高了程序的执行效率,减少了不必要的锁开销。
42、什么是锁粗化?
锁粗化 是一种针对连续的同步操作,将多个细粒度的锁合并成一个大的锁的优化技术。它的目的是减少线程在竞争同步锁时的竞争次数,从而提高程序的执行效率。
当某个线程频繁地对同一个对象进行加锁和解锁操作时,JVM 可能会将这些细粒度的锁操作合并成一个大的锁操作,从而减少线程在同步竞争上的开销。
锁粗化的实现原理:
在 JIT 编译过程中,编译器会对连续的加锁和解锁操作进行分析,如果发现这些操作之间没有其他的耗时操作,并且都是针对同一个对象的,那么就会将这些细粒度的锁操作合并成一个大的锁操作。
示例:
下面是一个简单的示例代码:
在上面的代码中,appendStrings
方法会对 StringBuilder
对象 sb
进行两次加锁和解锁操作。如果这个方法被频繁地调用,且调用之间没有其他的耗时操作,JVM 可能会将这两次加锁和解锁操作合并成一个大的锁操作,从而减少同步开销。
在这种情况下,如果 testLockCoarsening
方法被频繁调用,JVM 可能会将多次加锁和解锁操作合并成一个大的锁操作,提高了程序的执行效率。
锁粗化的好处:
- 减少同步开销:合并多个细粒度的锁操作,降低了同步竞争的开销。
- 提高程序性能:减少了线程在竞争同步锁时的竞争次数,提高了程序的执行效率。
总结:
锁粗化是一种针对连续的同步操作,将多个细粒度的锁合并成一个大的锁的优化技术。它通过减少同步开销,提高了程序的执行效率,特别是在对同一对象进行频繁加锁和解锁操作时,可以显著地减少同步竞争的开销。
43、什么是重量级锁?
重量级锁 是 Java 中的一种锁机制,用于解决多线程并发访问共享资源时的同步问题。重量级锁通常是指 传统的 synchronized 锁,由于它在 JVM 层面实现,并且涉及操作系统的监视器(monitor)机制,因而在某些情况下可能会引发性能开销,尤其是在锁争用严重时。
以下是对重量级锁的详细解释:
重量级锁的特性:
-
互斥:重量级锁保证在同一时刻只有一个线程可以获得锁,从而确保共享资源在多线程环境下的一致性和正确性。
-
内置锁:重量级锁是由 JVM 实现的,
synchronized
关键字是实现重量级锁的典型方式。当一个线程进入 synchronized 块或方法时,会自动获得锁;当线程退出同步块或方法时,会自动释放锁。 -
阻塞和唤醒:重量级锁在争用时会导致线程阻塞和唤醒,这涉及操作系统的上下文切换。被阻塞的线程进入等待队列,等待被唤醒重新竞争锁。这种阻塞和唤醒的机制会带来较高的性能开销。
-
锁的状态:重量级锁的状态存储在对象的头部(mark word)中,当锁被线程持有时,JVM 会将对象头的锁标记设置为重量级锁状态。
重量级锁的使用:
重量级锁通过 synchronized
关键字来实现,可以用于方法或者代码块:
-
同步方法:
-
同步代码块:
重量级锁的优化:
由于重量级锁的性能开销较大,Java 在 JVM 层面做了一些优化,主要包括:
-
偏向锁:当一个线程多次获取同一把锁时,会进入偏向模式,减少获取锁的开销。
-
轻量级锁:在锁没有争用的情况下,使用 CAS 操作来获取和释放锁,避免阻塞和唤醒的开销。
-
自旋锁:在短时间内通过自旋等待而不是阻塞,减少线程上下文切换的开销。
示例代码:
以下示例展示了如何使用 synchronized
实现重量级锁:
在这个示例中,多个线程会竞争 synchronizedMethod
和 synchronizedBlock
中的锁,展示了重量级锁的基本使用方式及其互斥特性。
44、什么是线程池?
线程池 是一种管理线程的机制,用于控制多个工作线程的使用,从而提高并发处理的效率和资源利用率。线程池通过复用一组预先创建的线程来处理任务,避免了频繁创建和销毁线程所带来的性能开销。线程池通常用于并发执行大量独立的任务,如处理网络请求、执行批处理作业等。
线程池的主要特性:
-
复用线程:线程池在初始化时创建一定数量的线程,并在任务执行完毕后将线程返回到池中以供再次使用,避免了频繁的线程创建和销毁。
-
任务排队:线程池内部维护一个任务队列,当所有线程都在执行任务时,新提交的任务会进入队列等待,直到有空闲线程可用。
-
资源管理:通过限制线程池中的最大线程数,线程池可以有效控制系统资源的使用,防止过多线程导致的资源耗尽和性能下降。
-
任务调度:线程池提供了灵活的任务调度机制,可以通过配置不同的任务队列和线程策略,实现复杂的任务调度逻辑。
线程池的使用:
Java 提供了 java.util.concurrent
包中的 Executor
框架来支持线程池的创建和管理。最常用的线程池实现是 ThreadPoolExecutor
。你可以通过 Executors
工具类来方便地创建常见类型的线程池。
示例代码:
下面是一个简单的示例,展示如何使用线程池执行任务:
在这个示例中:
- 使用
Executors.newFixedThreadPool(3)
创建一个固定大小的线程池,该线程池包含 3 个线程。 - 提交 5 个任务到线程池,线程池会分配线程来执行这些任务。由于线程池大小为 3,因此会有 3 个任务并发执行,其余任务在队列中等待。
- 通过
executorService.shutdown()
关闭线程池,当所有任务完成后,线程池将被关闭。
常见的线程池类型:
-
FixedThreadPool:固定大小的线程池,适用于已知并发量且任务执行时间较长的场景。
-
CachedThreadPool:根据需要创建新线程的线程池,适用于大量短期任务且并发量动态变化的场景。
-
ScheduledThreadPool:支持定时和周期性任务调度的线程池。
-
SingleThreadExecutor:单线程的线程池,适用于需要保证顺序执行任务的场景。
通过使用线程池,可以有效管理和优化多线程环境下的任务执行,提高系统的性能和稳定性。
45、使用线程池有什么好处?
使用线程池 有很多好处,尤其是在需要频繁创建和销毁线程的高并发环境中。以下是使用线程池的主要好处:
1. 提高性能:
- 减少创建销毁线程的开销:创建和销毁线程是昂贵的操作,线程池通过复用已经创建的线程,避免了频繁的创建和销毁操作,从而提高了系统性能。
- 优化资源使用:通过控制线程池中的最大线程数,可以避免过多线程同时运行导致的系统资源耗尽问题,如内存和 CPU 资源的过度使用。
2. 提高响应速度:
- 快速处理任务:由于线程池中的线程是预先创建好的,当有任务提交时可以立即执行,而无需等待线程创建,从而提高了任务处理的响应速度。
3. 更好的管理和监控:
- 线程管理:线程池提供了统一的接口来管理线程,可以方便地进行任务提交、取消和监控。
- 任务调度:可以通过配置不同类型的线程池和任务队列,实现灵活的任务调度和优先级管理。
4. 避免资源耗尽:
- 控制并发线程数:通过配置线程池的最大线程数,可以限制同时运行的线程数量,避免因过多线程导致的资源耗尽问题。
- 防止内存泄漏:合理使用线程池可以避免因频繁创建销毁线程而导致的内存泄漏问题。
5. 简化编程模型:
- 方便的API:Java 提供了
java.util.concurrent
包中的Executor
框架,简化了多线程编程的模型,使得线程的创建、管理和任务调度更加方便和灵活。 - 统一的异常处理:线程池可以统一管理线程中的异常处理,提高了代码的健壮性和可维护性。
6. 线程复用:
- 减少上下文切换:线程池中的线程在执行完一个任务后可以继续执行其他任务,减少了线程的上下文切换开销,提高了系统性能。
示例代码:
以下是一个使用线程池的示例,展示了如何通过线程池提高任务处理的效率和管理多个任务:
在这个示例中:
- 使用
Executors.newFixedThreadPool(3)
创建了一个固定大小的线程池,该线程池包含 3 个线程。 - 提交 5 个任务到线程池,线程池会分配线程来执行这些任务。由于线程池大小为 3,因此会有 3 个任务并发执行,其余任务在队列中等待。
- 通过
executorService.shutdown()
关闭线程池,当所有任务完成后,线程池将被关闭。
通过这种方式,线程池实现了对线程的高效管理和调度,提高了系统的并发处理能力和响应速度。
46、创建一个线程池有哪些核心参数?
创建一个线程池时,需要了解其核心参数,以便根据具体的应用场景进行优化配置。以下是 ThreadPoolExecutor
线程池的核心参数及其含义:
1. corePoolSize(核心线程池大小):
- 核心线程数是指线程池中始终保持活动的线程数,即使这些线程处于空闲状态。当有新的任务提交时,线程池会优先使用核心线程处理任务。
- 如果设置为0,当没有任务时,线程池将不保留任何线程。
2. maximumPoolSize(最大线程池大小):
- 最大线程数是指线程池中允许创建的最大线程数。当核心线程都在忙碌并且任务队列已满时,线程池会创建新线程来处理任务,但总线程数不会超过此最大值。
- 当设置的线程数超过最大值,任务将被拒绝执行。
3. keepAliveTime(线程空闲保持时间):
- 当线程池中的线程数超过核心线程数时,空闲线程在等待新任务时最多保持存活的时间。超过此时间,空闲线程将被终止并从线程池中移除。
- 该参数默认只对超出核心线程数的线程有效。通过
allowCoreThreadTimeOut(true)
方法也可以让核心线程在空闲时也受到此参数的控制。
4. unit(时间单位):
- 用于指定
keepAliveTime
参数的时间单位。常见的单位有 TimeUnit.SECONDS、TimeUnit.MILLISECONDS、TimeUnit.MINUTES 等。
5. workQueue(任务队列):
- 任务队列用于保存等待执行的任务。线程池中的线程在没有任务执行时会从此队列中获取新任务。常见的任务队列有:
ArrayBlockingQueue
:一个由数组结构组成的有界阻塞队列。LinkedBlockingQueue
:一个由链表结构组成的有界阻塞队列(若不指定大小,则为无界队列)。SynchronousQueue
:一个不存储元素的阻塞队列,每一个插入操作必须等待一个相应的删除操作。PriorityBlockingQueue
:一个具有优先级的无限阻塞队列。
6. threadFactory(线程工厂):
-
用于创建新线程。通过提供自定义的
ThreadFactory
,可以给每个创建的线程指定名字、设置优先级、设置是否为守护线程等。
7. handler(拒绝策略):
- 当任务队列已满且线程池中的线程数已达到最大值时,新任务会被拒绝。线程池提供了几种默认的拒绝策略:
AbortPolicy
:抛出 RejectedExecutionException 异常。CallerRunsPolicy
:由调用线程处理该任务。DiscardPolicy
:直接丢弃任务,不予处理。DiscardOldestPolicy
:丢弃队列中最旧的任务,并尝试重新提交新任务。
示例代码:
以下是一个使用 ThreadPoolExecutor
创建线程池的示例:
这个示例中:
- 创建了一个核心线程数为2、最大线程数为4的线程池。
- 线程在空闲超过10秒后将被回收。
- 使用
LinkedBlockingQueue
作为任务队列,容量为2。 - 自定义了
ThreadFactory
,为每个线程指定了名字。 - 使用默认的拒绝策略
AbortPolicy
,当任务被拒绝时抛出异常。
47、线程池的工作流程是怎样的?
线程池的工作流程 通常可以分为以下几个步骤:
1. 初始化线程池:
- 创建一个
ThreadPoolExecutor
对象,并配置核心线程数、最大线程数、空闲线程存活时间、任务队列、线程工厂和拒绝策略等参数。
2. 提交任务:
- 调用
execute(Runnable task)
或submit(Callable<V> task)
方法向线程池提交任务。
3. 任务处理流程:
- 任务队列:线程池首先检查运行的线程数量,如果少于核心线程数,则创建新的核心线程来执行任务;否则,将任务添加到任务队列中。
- 核心线程数:如果线程池中的线程数量少于核心线程数,即使有空闲线程,也会创建新的线程来处理任务。
- 任务执行:当任务队列已满且线程数量未达到最大线程数时,线程池会创建新的非核心线程来执行任务。
- 拒绝策略:如果任务队列已满且线程数量已达到最大线程数,则会根据配置的拒绝策略处理新提交的任务。
4. 任务执行完毕:
- 任务执行完成后,线程会返回线程池,如果线程池中的线程数量超过核心线程数并且空闲时间超过设置的
keepAliveTime
,则这些线程会被终止。
5. 关闭线程池:
- 调用
shutdown()
方法来启动线程池的关闭过程,线程池将不再接受新任务,但会继续执行已提交的任务。 - 调用
shutdownNow()
方法立即停止所有正在执行的任务,并尝试终止所有等待中的任务。
示例代码:
以下是一个使用 ThreadPoolExecutor
创建线程池并执行任务的示例:
工作流程解析:
-
初始化线程池:
- 创建一个
ThreadPoolExecutor
对象,核心线程数为2,最大线程数为4,空闲线程存活时间为10秒,使用LinkedBlockingQueue
作为任务队列,队列容量为2。
- 创建一个
-
提交任务:
- 通过
submit()
方法提交10个任务到线程池。
- 通过
-
任务处理:
- 线程池首先检查当前运行的线程数量。如果少于核心线程数2,则会创建新的核心线程来执行任务。
- 当核心线程数已满(2个核心线程),新任务将被加入到任务队列中(最多容纳2个任务)。
- 当任务队列也满了之后,且线程数未达到最大线程数4,线程池会创建新的线程来处理任务。
- 如果线程数已达到最大线程数4,新提交的任务将会根据拒绝策略处理,这里使用的是
AbortPolicy
,会抛出RejectedExecutionException
。
-
任务执行完毕:
- 任务执行完成后,线程返回线程池。如果线程池中的线程数量超过核心线程数且空闲时间超过10秒,这些线程将被终止。
-
关闭线程池:
- 调用
shutdown()
方法后,线程池将不再接受新任务,但会继续执行已提交的任务,直到所有任务执行完毕后关闭。
- 调用
48、Java 里面有哪些内置的线程池?
Java 中的内置线程池 主要由 java.util.concurrent.Executors
类提供。Executors
提供了一些静态方法来方便地创建和配置各种类型的线程池。以下是几种常见的内置线程池:
1. newFixedThreadPool(int nThreads)
- 创建一个固定大小的线程池,该线程池中的线程数保持固定。
- 如果所有线程都在忙碌,新任务会在队列中等待,直到有线程可用。
- 适用于任务数量已知且相对固定的场景。
示例:
2. newCachedThreadPool()
- 创建一个可缓存的线程池,根据需要创建新线程,空闲线程会被回收。
- 适用于执行许多短期异步任务的小程序,或负载较轻的服务器。
- 如果线程池的规模超过处理任务所需的线程数量,则回收空闲的线程;当需求增加时,可以添加新线程。
示例:
3. newSingleThreadExecutor()
- 创建一个单线程的线程池,线程池中只有一个线程。
- 适用于需要保证任务顺序执行的场景。
- 保证所有任务都在同一个线程中按顺序执行,不会并发执行。
示例:
4. newScheduledThreadPool(int corePoolSize)
- 创建一个支持定时和周期性任务执行的线程池。
- 适用于需要周期性执行任务或定时调度任务的场景。
示例:
5. newWorkStealingPool(int parallelism)
- 创建一个并行级别为给定值的工作窃取线程池,使用所有可用的处理器作为其目标并行级别。
- 适用于需要大量并行执行任务的场景。
- 该线程池基于工作窃取算法,能够有效地处理大量的独立任务。
示例:
示例代码:
以下是一个使用各种内置线程池的示例代码:
这个示例展示了如何使用 Java 内置的不同类型的线程池来执行任务。每种线程池都有其适用的场景和特点,根据具体的需求选择合适的线程池可以提高程序的效率和性能。
49、为什么阿里不让用 Executors 创建线程池?
阿里巴巴不推荐使用 Executors
创建线程池的原因 主要是因为 Executors
提供的线程池创建方法存在一些潜在的风险,可能会导致系统资源耗尽或性能问题。因此,阿里巴巴推荐使用 ThreadPoolExecutor
进行线程池的创建和配置。
以下是 Executors
提供的线程池创建方法存在的潜在问题:
1. newFixedThreadPool
和 newSingleThreadExecutor
:
- 这两种线程池的
workQueue
使用的是LinkedBlockingQueue
,它是一个无界队列。如果任务提交速度远远超过任务处理速度,任务会不断累积,可能导致内存溢出(OOM)。
2. newCachedThreadPool
:
- 该线程池的最大线程数是
Integer.MAX_VALUE
,在极端情况下可能会创建大量线程,导致系统资源耗尽(例如,CPU和内存)。
3. newScheduledThreadPool
:
- 默认的
ScheduledThreadPoolExecutor
也使用无界队列,可能会导致同样的问题。
阿里巴巴的推荐方式:
- 使用
ThreadPoolExecutor
显式地设置核心线程数、最大线程数、空闲线程存活时间、任务队列和拒绝策略等参数,避免使用无界队列和过大的线程数。
示例代码:
以下是一个使用 ThreadPoolExecutor
进行线程池创建的示例代码:
解释:
1. 设置了核心线程数和最大线程数:
- 核心线程数为2,最大线程数为4。
2. 使用了有界队列:
- 任务队列使用的是
ArrayBlockingQueue
,其容量为2,防止任务过多导致内存溢出。
3. 配置了自定义线程工厂:
- 使用自定义的
CustomThreadFactory
创建线程,以便更好地管理线程的命名和属性。
4. 配置了拒绝策略:
- 使用
ThreadPoolExecutor.AbortPolicy
作为拒绝策略,当任务无法提交到线程池时抛出RejectedExecutionException
。
通过这种方式,可以更好地控制线程池的行为,避免因资源耗尽或性能问题导致系统崩溃。
50、线程池的拒绝策略有哪几种?
线程池的拒绝策略 是指当线程池无法处理新的任务时采取的处理方式。Java 的 ThreadPoolExecutor
提供了以下几种内置的拒绝策略:
1. AbortPolicy
:
-
默认的拒绝策略。直接抛出
RejectedExecutionException
异常,阻止系统正常工作。
2. CallerRunsPolicy
:
-
只要线程池没有关闭,该策略直接在调用者线程中运行当前被丢弃的任务。这种策略提供了一个简单的反馈控制机制,可以减缓新任务的提交速度。
3. DiscardPolicy
:
-
该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是合适的策略。
4. DiscardOldestPolicy
:
-
该策略将丢弃最老的未处理任务,然后尝试重新提交当前任务。如果任务队列中有等待很久的任务,可以使用该策略。
自定义拒绝策略:
-
你还可以通过实现
RejectedExecutionHandler
接口来自定义拒绝策略。例如,你可以记录日志或者将任务保存到其他队列中,以便稍后再处理。
示例代码:
以下是一个使用不同拒绝策略的线程池示例:
在这个示例中,当线程池的任务队列满时,会使用 AbortPolicy
拒绝策略直接抛出 RejectedExecutionException
异常并打印相关信息。你可以将 AbortPolicy
更改为其他策略以查看不同的拒绝行为。
51、如何提交一个线程到线程池?
提交一个线程到线程池 可以通过使用 ExecutorService
接口的 submit
或 execute
方法。以下是两种常用方法的详细解释和示例:
1. 使用 execute
方法提交任务:
execute
方法用于提交一个Runnable
任务,并且不会返回任何结果。该方法适用于不需要任务返回结果的情况。
示例代码:
2. 使用 submit
方法提交任务:
submit
方法可以提交Runnable
或Callable
任务,并返回一个Future
对象。Future
对象可以用于检查任务的执行状态和获取任务的结果。
提交 Runnable
任务的示例:
提交 Callable
任务的示例:
总结:
execute
方法:适用于不需要任务返回结果的情况。submit
方法:适用于需要获取任务执行状态或结果的情况。可以提交Runnable
或Callable
任务,并返回一个Future
对象。
52、线程池 submit 和 execute 有什么区别?
线程池的 submit
和 execute
方法的区别:
1. execute
方法:
execute(Runnable command)
方法用于提交一个实现了Runnable
接口的任务,并且不会返回任何结果。这个方法主要用于不需要返回值的异步任务。
2. submit
方法:
-
submit(Runnable task)
:提交一个Runnable
任务,返回一个Future
对象,可以通过Future
对象检查任务的状态或取消任务,但因为Runnable
没有返回值,所以Future.get()
方法会返回null
。 -
submit(Callable<T> task)
:提交一个Callable
任务,返回一个Future
对象,可以通过Future
对象获取任务的执行结果或异常。
主要区别:
1. 返回值:
execute
方法没有返回值,适用于不需要返回结果的任务。submit
方法返回一个Future
对象,可以用于获取任务的执行结果或取消任务。
2. 任务类型:
execute
方法只能提交实现了Runnable
接口的任务。submit
方法可以提交实现了Runnable
接口的任务,也可以提交实现了Callable
接口的任务。
3. 异常处理:
execute
方法如果任务在执行过程中抛出异常,该异常会直接传播到调用线程。submit
方法如果任务在执行过程中抛出异常,该异常会被捕获并存储在返回的Future
对象中,可以通过调用Future.get()
方法时获得。
示例代码:
execute
方法示例:
submit
方法示例:
总结:execute
方法更简单直接,适用于不需要返回结果的任务;submit
方法功能更强大,适用于需要返回结果或处理异常的任务。
53、如何查看线程池的运行状态?
要查看线程池的运行状态,可以通过 ThreadPoolExecutor
类提供的方法来获取线程池的各种状态信息。以下是一些常用的方法:
-
获取线程池的基本信息:
-
获取线程池的状态:
-
获取线程池队列信息:
-
其他方法:
getPoolSize()
:获取当前线程池中的线程数。getLargestPoolSize()
:获取线程池曾经达到的最大线程数。getKeepAliveTime(TimeUnit unit)
:获取线程空闲超时时间。allowsCoreThreadTimeOut()
:判断核心线程是否允许超时。- 等等。
这些方法可以帮助你了解线程池的运行状态、任务队列情况以及线程池是否处于关闭或终止状态。
54、如何设置线程池的大小?
要设置线程池的大小,可以使用 ThreadPoolExecutor
类的构造方法或者通过 setCorePoolSize
和 setMaximumPoolSize
方法来进行设置。
方法一:使用构造方法设置线程池大小
方法二:使用 setCorePoolSize
和 setMaximumPoolSize
方法设置线程池大小
在这两种方法中,需要注意以下几点:
- 核心线程数:核心线程数决定了线程池的基本容量,即线程池中始终保持的线程数量。如果线程池中的线程数量少于核心线程数,则新任务将创建新线程来处理。
- 最大线程数:最大线程数是线程池中允许的最大线程数量,包括核心线程数和额外创建的线程数。当任务队列满了并且线程池中的线程数小于最大线程数时,新任务将创建新线程来处理。
- 任务队列:任务队列用于存放等待执行的任务。可以使用不同类型的任务队列,例如
LinkedBlockingQueue
、ArrayBlockingQueue
等。
根据实际需求,合理设置线程池的大小可以提高系统的性能和资源利用率。
55、如何关闭线程池?
要关闭线程池,可以使用 shutdown
或 shutdownNow
方法。这两种方法的区别在于:
-
shutdown
方法:该方法会停止接收新任务,并尝试将已经提交但尚未开始执行的任务执行完成。已经开始执行的任务不受影响,会继续执行直到完成。 -
shutdownNow
方法:该方法会停止接收新任务,并尝试中断正在执行的任务,并返回未执行的任务列表。这个方法会比较突然地中断任务,可能会引起一些任务的异常处理。
下面是使用这两种方法关闭线程池的示例:
使用 shutdown
方法关闭线程池:
使用 shutdownNow
方法关闭线程池:
在使用 shutdownNow
方法时,会返回一个未执行的任务列表,可以根据需要进行处理。需要注意的是,关闭线程池后就无法再向线程池提交新的任务了。
56、AQS 是什么?
AQS(AbstractQueuedSynchronizer)是 Java 中用于实现同步器的抽象基类,它提供了一个框架,可以用来构建各种类型的同步器,例如锁、信号量、倒计时器等。AQS 使用一种称为"CLH 锁队列"的数据结构来管理线程之间的竞争关系,并通过内部的状态来控制线程的获取和释放。在 Java 中,ReentrantLock
、Semaphore
、CountDownLatch
等同步器都是基于 AQS 实现的。
AQS 的主要特点包括:
- 内部状态:AQS 通过一个整型变量来表示同步状态,通常用于表示资源是否被占用、可用资源数量等信息。
- 等待队列:AQS 使用一个双向链表作为等待队列,用来存储等待获取资源的线程,并采用 CLH(Craig, Landin, and Hagersten)锁队列来管理线程之间的竞争关系。
- 独占模式和共享模式:AQS 支持两种同步模式,独占模式(Exclusive Mode)和共享模式(Shared Mode)。独占模式下只有一个线程可以获得同步状态,而共享模式下允许多个线程同时获得同步状态。
- CAS 操作:AQS 使用 CAS(Compare and Swap)操作来更新同步状态,保证线程安全性。
AQS 的核心方法包括:
acquire(int arg)
:尝试获取同步状态,如果成功则返回,否则进入等待队列并阻塞当前线程。release(int arg)
:释放同步状态,唤醒等待队列中的线程。
通过实现 AQS 的子类,并重写 tryAcquire
、tryRelease
等方法,可以实现各种自定义的同步器。AQS 提供了一种高效且灵活的同步机制,为 Java 中的同步工具提供了强大的支持。
57、AQS 的底层原理是什么?
AQS(AbstractQueuedSynchronizer)的底层原理涉及到几个重要的概念和数据结构:
-
CLH 锁队列:AQS 使用 CLH(Craig, Landin, and Hagersten)锁队列作为等待队列的数据结构。CLH 锁队列是一种自旋锁队列,每个等待线程都会持有一个节点(Node),通过这些节点形成一个双向链表。当一个线程需要获取锁时,会在队列尾部添加自己的节点,并且将前一个节点作为前驱节点,然后自旋等待前驱释放锁。
-
同步状态(state):AQS 使用一个整型变量来表示同步状态,通常用于表示资源是否被占用、可用资源数量等信息。同步状态可以通过
getState
和setState
方法来获取和设置。 -
共享模式和独占模式:AQS 支持两种同步模式,独占模式(Exclusive Mode)和共享模式(Shared Mode)。独占模式下只有一个线程可以获得同步状态,而共享模式下允许多个线程同时获得同步状态。
-
CAS 操作:AQS 使用 CAS(Compare and Swap)操作来更新同步状态,保证线程安全性。通过 CAS 操作来实现对同步状态的原子更新,避免了使用锁的方式来保护同步状态,提高了并发性能。
-
等待队列和条件队列:AQS 中有两个队列,一个是等待队列(Wait Queue),用于存放等待获取同步状态的线程;另一个是条件队列(Condition Queue),用于支持
Condition
接口相关的条件等待和通知。
基于以上原理,AQS 的工作流程大致如下:
- 当一个线程尝试获取锁时,会先通过 CAS 操作尝试更新同步状态,如果成功则获取锁并返回;否则将当前线程封装成节点并加入到 CLH 锁队列的末尾,并且自旋等待前驱节点释放锁。
- 当持有锁的线程释放锁时,会通过 CAS 操作更新同步状态,并且唤醒等待队列中的线程。
- 在独占模式下,只有一个线程可以成功获取锁,其他线程会进入等待队列排队;而在共享模式下,多个线程可以同时获取同步状态。
总的来说,AQS 的底层原理利用了 CAS 操作、CLH 锁队列、自旋等待等技术来实现高效的同步器,为 Java 中的同步工具提供了强大的支持。
58、Java 中的 Fork Join 框架有什么用?
Java 中的 Fork/Join 框架是用于并行计算的框架,主要用于处理递归式任务的并行化执行,例如分治算法。它的主要作用是将大型任务拆分成多个小任务,并行执行这些小任务,然后合并结果,从而提高计算效率和性能。
Fork/Join 框架的主要用途和优势包括:
-
并行计算:Fork/Join 框架可以将一个大任务拆分成多个小任务,这些小任务可以并行执行,从而充分利用多核处理器的性能优势,加速任务的执行速度。
-
递归任务处理:适用于递归式的任务处理,例如分治算法、归并排序等,将任务不断拆分成子任务并行执行,然后合并结果。
-
减少线程竞争:Fork/Join 框架采用工作窃取(Work Stealing)算法,每个线程都有自己的任务队列,当一个线程执行完自己的任务后,可以从其他线程的队列中窃取任务执行,减少了线程之间的竞争,提高了并行度和效率。
-
简化并行编程:Fork/Join 框架的 API 设计简单易用,通过继承
RecursiveTask
或RecursiveAction
类并重写compute
方法,可以轻松实现并行计算任务。 -
提高程序性能:合理使用 Fork/Join 框架可以提高程序的性能,特别是对于需要处理大量数据或复杂计算的场景,可以显著减少计算时间。
总的来说,Fork/Join 框架适用于处理递归式的任务,并行计算大型数据集或复杂计算,提高程序的并行性和性能。
59、ThreadLocal 有什么用?
ThreadLocal
是 Java 中的一个线程级别的变量,它提供了一种在每个线程中存储和获取变量副本的机制。每个线程都可以独立地访问自己的变量副本,互不干扰,从而实现了线程间的数据隔离。ThreadLocal
主要用于以下几个方面:
-
线程上下文信息的存储:
ThreadLocal
可以用来存储线程的上下文信息,例如用户身份、请求信息等。这样在多个方法之间传递这些信息时就不需要显式传递参数,减少了方法之间的耦合性。 -
线程内部状态的管理:某些情况下,需要在同一个线程中共享一些状态信息,但不希望将这些信息暴露到方法参数中。这时可以使用
ThreadLocal
来管理线程内部的状态,各个方法可以通过ThreadLocal
获取或修改这些状态信息。 -
线程安全的对象存储:
ThreadLocal
在多线程环境下可以实现线程安全的对象存储。每个线程访问自己的ThreadLocal
对象时,都可以获取到独立的对象副本,避免了线程安全问题。 -
避免传递参数的繁琐:在某些场景下,如果需要在多个方法中传递相同的参数,通过
ThreadLocal
可以避免参数传递的繁琐,使代码更加简洁清晰。
需要注意的是,ThreadLocal
应该谨慎使用,过度使用可能会导致内存泄漏或产生意外的结果。特别是在线程池环境下,需要注意及时清理 ThreadLocal
变量,避免长时间占用线程变量导致的资源浪费。
60、ThreadLocal 有什么副作用?
使用 ThreadLocal
时需要注意以下副作用:
-
内存泄漏:如果使用不当,
ThreadLocal
变量可能导致内存泄漏。因为ThreadLocal
是与线程相关联的,如果不及时清理ThreadLocal
变量,可能会导致变量长时间占用线程内存,影响系统性能。 -
数据隔离不彻底:
ThreadLocal
虽然可以实现线程间数据隔离,但在一些场景下可能会造成数据泄露或共享的问题。如果在一个线程中多次使用相同的ThreadLocal
变量,可能会造成前后数据混乱的情况。 -
不可控的生命周期:
ThreadLocal
的生命周期由线程决定,一旦线程结束,ThreadLocal
中的变量也会被销毁。如果不注意及时清理ThreadLocal
变量,可能会造成意外的结果。 -
对性能的影响:虽然
ThreadLocal
可以提高程序的性能,减少锁竞争,但过度使用ThreadLocal
可能会影响系统性能,特别是在线程池环境下。因为每个ThreadLocal
变量都会在线程中占用一定的内存,如果有大量的ThreadLocal
变量,可能会导致内存占用过高。
为了避免 ThreadLocal
的副作用,可以注意以下几点:
- 及时清理:在使用完
ThreadLocal
变量后,及时调用remove()
方法清理,避免长时间占用线程内存。 - 合理使用:避免过度使用
ThreadLocal
,只在需要线程隔离的场景下使用。 - 注意线程池环境:在使用线程池时,要注意
ThreadLocal
变量的生命周期与线程池的管理方式,避免资源浪费或内存泄漏。
综上所述,ThreadLocal
在提高程序性能和简化代码的同时,也需要谨慎使用,避免出现副作用。
61、volatile 关键字有什么用?
volatile
关键字在 Java 中主要用于保证变量的可见性、禁止指令重排序,以及一定程度上保证变量的原子性操作。具体来说,volatile
的作用包括以下几个方面:
-
保证可见性:当一个变量被声明为
volatile
时,线程在读取该变量的值时会直接从主内存中读取,而不是从线程的本地缓存中读取。这样可以保证当一个线程修改了该变量的值后,其他线程能够立即看到修改后的值,避免了线程间的数据不一致性问题。 -
禁止指令重排序:
volatile
关键字会禁止编译器对被修饰变量进行指令重排序优化。这样可以确保在多线程环境下,指令执行的顺序不会发生变化,从而保证程序的正确性。 -
部分原子性操作:
volatile
可以保证对volatile
变量的写操作是原子性的。但是需要注意的是,volatile
不能保证复合操作的原子性,例如i++
这种操作不是原子性的。
总的来说,volatile
关键字适用于以下场景:
- 在多线程环境下,需要保证变量的可见性,避免线程间数据不一致。
- 需要禁止指令重排序,保证程序执行的顺序。
- 需要进行一些简单的原子性操作,例如标志位的设置和获取。
需要注意的是,虽然 volatile
可以保证可见性和禁止指令重排序,但并不能保证线程安全。在需要线程安全的场景下,仍然需要使用锁或其他同步机制来保证数据的一致性和正确性。
62、volatile 有哪些应用场景?
volatile
关键字主要适用于以下几个应用场景:
-
标记状态变量:
volatile
变量适用于标记状态的变化,例如线程中止标志位、任务是否完成的标志等。通过将这些标志位声明为volatile
变量,可以保证各个线程及时看到状态的变化,从而做出相应的处理。 -
单例模式中的双重检查锁定(Double-Checked Locking):在单例模式中,使用
volatile
可以确保线程安全地创建单例对象,并且保证对象的可见性。 -
轻量级的线程同步控制:
volatile
变量可以作为一种轻量级的线程同步机制,用于某些不需要复杂同步的场景,例如计数器、标志位等。 -
事件驱动机制:在一些事件驱动的系统中,可以使用
volatile
变量来通知线程事件的发生或处理状态的变化,例如线程池中的任务完成通知。
总的来说,volatile
主要适用于需要保证变量可见性、禁止指令重排序的场景,以及一些简单的原子性操作。需要注意的是,volatile
并不能替代锁或其他同步机制,它只能保证一定程度的线程安全性,对于复合操作或需要复杂同步的场景,仍然需要使用锁来保证线程安全。
63、CyclicBarrier 有什么用?
CyclicBarrier
(循环屏障)是 Java 并发包中的一个同步工具,主要用于在多个线程之间创建一个同步点,当所有线程都到达这个同步点时,才能继续执行后续操作。它的主要作用包括以下几个方面:
-
同步多个线程:
CyclicBarrier
可以用于同步多个线程,让它们在某个点上进行等待,直到所有线程都到达这个点,才能继续往下执行。 -
阶段性任务的并行计算:
CyclicBarrier
可以用于将一个大任务拆分成多个阶段,每个线程负责执行一个阶段的任务,在每个阶段结束时等待其他线程完成,然后再进行下一个阶段的计算,从而实现任务的并行计算。 -
流水线工作模式:在流水线工作模式中,多个工人分别完成流水线上的不同任务,每个工人完成自己的任务后,需要等待其他工人完成,然后一起进行下一个任务。这种情况下可以使用
CyclicBarrier
来同步工人的工作。 -
多线程计算的结果合并:在多线程计算中,每个线程计算一部分结果,最后需要将所有结果合并。
CyclicBarrier
可以用于等待所有线程计算完成,然后合并结果。 -
多线程协作:
CyclicBarrier
可以用于多个线程之间的协作,例如多个线程分别读取不同的文件内容,然后将内容合并或处理后再进行下一步操作。
总的来说,CyclicBarrier
主要用于实现多个线程之间的同步和协作,让它们在特定的点上进行等待,直到所有线程都到达这个点,才能继续执行后续操作,从而实现多线程任务的协同工作。
64、CountDownLatch 有什么用?
CountDownLatch
(倒计时门闩)是 Java 并发包中的一个同步工具,用于控制多个线程之间的协作。它的主要作用是在某个线程等待其他多个线程完成后再继续执行,可以用于实现多线程任务的并行计算、任务分片处理等场景。CountDownLatch
的用途包括以下几个方面:
-
等待多个线程完成:
CountDownLatch
可以让一个线程等待其他多个线程都执行完毕后再继续执行。通过设置计数器的初始值,并在每个线程执行完毕时调用countDown()
方法来减少计数器的值,当计数器值减为0时,等待的线程可以继续执行。 -
多线程任务的并行计算:
CountDownLatch
可以用于将一个大任务拆分成多个子任务,并行执行这些子任务,等所有子任务都执行完毕后再进行结果合并或下一步操作。 -
任务分片处理:在处理大量数据或复杂任务时,可以将任务分成多个片段,每个线程处理一个片段,等所有片段处理完毕后再进行结果汇总或下一步操作。
-
流水线工作模式:类似于流水线工作模式,每个工人处理一部分任务,等所有工人都完成任务后再进行下一步操作。
-
多线程并发控制:
CountDownLatch
可以用于控制多个线程的并发数量,例如只有在某个线程池中的线程都执行完毕后,才能继续执行其他操作。
总的来说,CountDownLatch
主要用于实现多个线程之间的同步和协作,控制线程的执行顺序和并发数量,从而实现复杂任务的分解和协同工作。
65、CountDownLatch 与 CyclicBarrier 的区别?
CountDownLatch
(倒计时门闩)和 CyclicBarrier
(循环屏障)是 Java 并发包中的两个同步工具,它们都可以用于实现多个线程之间的协作和同步,但在功能和使用方式上有一些区别:
-
功能区别:
CountDownLatch
主要用于一个线程等待其他多个线程都执行完毕后再继续执行的场景。它通过一个计数器来控制线程之间的协作,计数器初始值设置为等待的线程数量,每个线程执行完毕后调用countDown()
方法来减少计数器的值,当计数器值减为0时,等待的线程可以继续执行。CyclicBarrier
主要用于多个线程之间的相互等待,直到所有线程都到达某个同步点后再继续执行后续操作。它也可以通过一个计数器来控制线程之间的协作,但计数器的初始值设置为线程数量,每个线程执行到同步点时调用await()
方法进行等待,直到所有线程都到达同步点后,计数器归零,所有线程可以继续执行。
-
循环性:
CountDownLatch
是单次性的,一旦计数器减为0,就无法重置,后续再次使用需要重新创建新的CountDownLatch
对象。CyclicBarrier
是可以循环使用的,当所有线程都到达同步点后,计数器会重置为初始值,可以继续进行下一轮的同步等待。
-
使用方式:
CountDownLatch
的使用比较简单,只需要设置计数器初始值,并在需要等待的线程调用await()
方法等待,其他线程执行完毕后调用countDown()
方法减少计数器值即可。CyclicBarrier
的使用相对复杂一些,需要在构造方法中指定参与同步的线程数量,并在每个线程执行到同步点时调用await()
方法等待,所有线程到达同步点后,会自动触发后续操作。
总的来说,CountDownLatch
和 CyclicBarrier
都可以用于实现线程间的同步和协作,但在功能和使用方式上略有不同,根据具体的场景选择合适的同步工具。CountDownLatch
更适合于单次性的线程等待,而 CyclicBarrier
更适合于循环使用的线程同步。
66、Semaphore 有什么用?
Semaphore
(信号量)是 Java 并发包中的一个同步工具,它主要用于控制对共享资源的访问数量,可以用于限流、资源池管理、多线程协作等场景。Semaphore
的主要作用包括以下几个方面:
-
限流:
Semaphore
可以用于限制某个系统或服务的并发访问数量,控制同时访问该系统或服务的线程数量,防止过多的请求导致系统资源耗尽或性能下降。 -
资源池管理:
Semaphore
可以用于管理资源池,例如连接池、线程池等,限制同时获取资源的线程数量,防止资源被过度消耗或耗尽。 -
多线程协作:
Semaphore
可以用于实现多个线程之间的协作,例如生产者消费者模式中控制生产者和消费者的比例,或者限制多个线程对某个共享资源的访问数量。 -
并发访问控制:
Semaphore
可以用于控制对某个共享资源的并发访问数量,例如数据库连接池中限制同时访问数据库的连接数量。
总的来说,Semaphore
主要用于控制并发访问数量,通过管理许可证数量来限制对共享资源的访问,从而实现资源的合理分配和保护。
67、Exchanger 有什么用?
Exchanger
(交换器)是 Java 并发包中的一个同步工具,主要用于两个线程之间交换数据。它提供了一个同步点,在这个同步点上两个线程可以交换彼此的数据,当两个线程都到达同步点时,它们可以交换数据并继续执行后续操作。Exchanger
的主要作用包括以下几个方面:
-
线程间数据交换:
Exchanger
主要用于实现两个线程之间的数据交换,其中一个线程调用exchange()
方法传递数据,另一个线程也调用exchange()
方法接收数据,两个线程在同一个Exchanger
上进行数据交换。 -
协同工作:
Exchanger
可以用于实现多个线程之间的协同工作,例如生产者消费者模式中,生产者线程生产数据并交换给消费者线程进行消费,从而实现生产者和消费者之间的协作。 -
数据同步:
Exchanger
可以用于同步数据的读取和写入,例如一个线程负责读取数据,另一个线程负责写入数据,在同步点上进行数据的交换和同步,保证数据的一致性。 -
任务分工:
Exchanger
可以用于实现任务分工,例如一个线程负责收集数据,另一个线程负责处理数据,通过Exchanger
在合适的时机进行数据交换,实现任务的分工和协同。
总的来说,Exchanger
主要用于实现两个线程之间的数据交换和协作,提供了一个同步点,使得两个线程可以在同步点上进行数据的交换和同步,从而实现多线程间的协同工作和数据交换。
68、LockSupport 有什么用?
LockSupport
是 Java 并发包中的一个工具类,主要用于线程的阻塞和唤醒操作。它提供了线程级别的阻塞和唤醒功能,可以用于实现线程的等待和通知机制。LockSupport
的主要作用包括以下几个方面:
-
线程阻塞:
LockSupport
可以让线程进入阻塞状态,暂停线程的执行,等待特定的条件满足后再继续执行。可以通过park()
方法使线程阻塞。 -
线程唤醒:
LockSupport
可以唤醒处于阻塞状态的线程,使其恢复执行。可以通过unpark(Thread thread)
方法唤醒指定线程,或者通过unpark()
方法唤醒当前线程。 -
线程挂起与恢复:
LockSupport
可以实现线程的挂起和恢复,使线程暂停执行和恢复执行。 -
替代传统的 wait/notify 机制:
LockSupport
可以替代传统的 wait/notify 机制,提供了更灵活和可靠的线程阻塞和唤醒操作。
总的来说,LockSupport
主要用于线程的阻塞和唤醒操作,可以实现线程的等待和通知机制,提供了一种灵活、可靠的线程控制方式。它是并发编程中常用的工具之一,可以有效地管理线程的执行状态。
69、Java 中原子操作的类有哪些?
Java 中原子操作的类主要包括以下几种:
- AtomicBoolean:原子更新布尔类型的类。
- AtomicInteger:原子更新整型的类。
- AtomicLong:原子更新长整型的类。
- AtomicReference:原子更新引用类型的类。
- AtomicIntegerArray:原子更新整型数组的类。
- AtomicLongArray:原子更新长整型数组的类。
- AtomicReferenceArray:原子更新引用类型数组的类。
- AtomicIntegerFieldUpdater:原子更新整型字段的类。
- AtomicLongFieldUpdater:原子更新长整型字段的类。
- AtomicReferenceFieldUpdater:原子更新引用类型字段的类。
这些原子操作的类提供了一种线程安全的方式来更新对应类型的数据,保证了在多线程环境下的原子性操作,避免了因并发访问而引发的数据不一致问题。通过这些类,可以实现更加高效和安全的并发编程。
70、什么是 ABA 问题?怎么解决?
ABA 问题是指在多线程环境下,一个共享变量的值从 A 变成了 B,然后再变回 A,而在这个过程中可能会引起误判。具体来说,ABA 问题可能会导致某些线程在比较共享变量时发生错误,因为它们只关注变量的当前值,并不考虑变量的修改历史。
举个例子来说,假设共享变量初始值为 A,然后被线程 1 修改为 B,接着又被线程 2 修改回 A。此时如果某个线程在比较共享变量时只看到了 A,并不知道它的修改历史,可能会错误地认为变量没有被其他线程修改过。
为了解决 ABA 问题,可以使用以下几种方法:
-
版本号或时间戳:在修改共享变量时,同时记录一个版本号或时间戳,每次修改都更新版本号或时间戳。在比较共享变量时,不仅比较值,还要比较版本号或时间戳,从而避免 ABA 问题。
-
CAS(Compare and Swap)算法:CAS 是一种乐观锁机制,在更新共享变量时,先比较当前值是否与期望值相等,如果相等则进行更新,否则重新尝试。CAS 可以通过版本号或时间戳来解决 ABA 问题。
-
ABA 问题的检测和处理:某些情况下,可以通过检测共享变量的修改历史来解决 ABA 问题,例如使用带有版本号的数据结构来记录修改历史,从而及时发现 ABA 问题并进行处理。
总的来说,解决 ABA 问题的关键是要在比较共享变量时考虑变量的修改历史,而不仅仅是当前值。使用版本号、时间戳、CAS 等技术可以有效地避免或处理 ABA 问题。
71、Java 并发容器,你知道几个?
Java 并发包中提供了许多并发容器,常用的几个包括:
-
ConcurrentHashMap:线程安全的哈希表,用于替代传统的 HashMap,在高并发场景下性能更好。
-
CopyOnWriteArrayList:线程安全的动态数组,通过复制的方式来保证线程安全,适用于读多写少的场景。
-
CopyOnWriteArraySet:线程安全的集合,基于 CopyOnWriteArrayList 实现,适用于读多写少的场景。
-
ConcurrentLinkedQueue:线程安全的队列,使用非阻塞算法实现,适用于高并发的队列操作。
-
ConcurrentLinkedDeque:线程安全的双端队列,使用非阻塞算法实现,支持在队头和队尾进行高效的插入和删除操作。
-
BlockingQueue:阻塞队列接口,提供了在队列为空或队列满时阻塞等待的功能,常见的实现类包括 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等。
-
ConcurrentSkipListMap:线程安全的跳表映射,支持并发操作,并且有序,适用于高并发的有序映射操作。
-
ConcurrentSkipListSet:线程安全的跳表集合,基于 ConcurrentSkipListMap 实现,支持并发操作,并且有序。
这些并发容器都提供了线程安全的操作,可以在多线程环境下使用,提高了程序的并发性能和安全性。具体选择哪个并发容器取决于需求和场景,不同的并发容器有不同的特点和适用范围。
72、什么是阻塞队列?
阻塞队列(Blocking Queue)是 Java 并发包中提供的一种特殊类型的队列,它具有阻塞等待的特性。阻塞队列在队列为空或队列满时,会阻塞插入或移除元素的操作,直到条件满足后再继续执行。这种阻塞等待的特性使得阻塞队列非常适合于生产者消费者模式和线程池等场景。
阻塞队列的特点包括:
-
线程安全:阻塞队列是线程安全的,多个线程可以同时操作队列而不需要额外的同步措施。
-
阻塞等待:在队列为空时,尝试从队列中取出元素的操作会被阻塞,直到队列不为空;在队列已满时,尝试向队列中添加元素的操作会被阻塞,直到队列有空闲空间。
-
支持多种操作:阻塞队列支持多种插入和移除元素的操作,例如插入、移除、检查队首元素、检查队尾元素等。
-
适用于生产者消费者模式:阻塞队列非常适合于实现生产者消费者模式,生产者线程可以将数据放入队列中,消费者线程可以从队列中取出数据进行处理,而不需要手动进行线程间的协调和同步。
常见的阻塞队列实现类包括:
- ArrayBlockingQueue:基于数组实现的有界阻塞队列,可以指定队列的容量。
- LinkedBlockingQueue:基于链表实现的可选有界或无界阻塞队列,如果不指定容量,默认为无界队列。
- PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,元素按照优先级顺序进行排序。
阻塞队列在多线程编程中扮演着重要的角色,能够有效地管理线程之间的数据交换和同步,提高程序的并发性能和可靠性。
73、阻塞队列有哪些常用的应用场景?
阻塞队列由于其特性,在多线程编程中有许多常用的应用场景,包括但不限于以下几种:
-
生产者消费者模式:阻塞队列非常适合于实现生产者消费者模式。生产者线程向队列中放入数据,消费者线程从队列中取出数据进行处理,队列的阻塞特性可以有效地协调生产者和消费者的速度,避免数据溢出或丢失。
-
线程池任务队列:线程池通常会使用阻塞队列作为任务队列,用于存放待执行的任务。当线程池中的线程数量达到上限时,新提交的任务会被放入阻塞队列中,等待线程池中的线程空闲时取出执行。
-
网络编程中的消息队列:在网络编程中,可以使用阻塞队列作为消息队列,用于存放接收到的消息。当消息处理线程忙于处理某个消息时,新接收到的消息会被放入阻塞队列中,等待处理。
-
任务调度器的任务队列:任务调度器通常会使用阻塞队列作为任务队列,用于存放待执行的任务。调度器将任务按照优先级放入队列中,待调度的线程会从队列中取出任务执行。
-
多线程协作中的同步工具:阻塞队列可以作为同步工具,用于多个线程之间的协作。例如,一个线程等待另一个线程完成某个任务后才能继续执行,可以使用阻塞队列来实现线程间的等待和通知。
总的来说,阻塞队列在多线程编程中扮演着重要的角色,能够有效地管理线程之间的数据交换和同步,提高程序的并发性能和可靠性。
74、Java 中的阻塞的队列有哪些?
Java 中常用的阻塞队列包括以下几种:
-
ArrayBlockingQueue:基于数组实现的有界阻塞队列,可以指定队列的容量。在队列为空时,尝试从队列中取出元素的操作会被阻塞,直到队列不为空;在队列已满时,尝试向队列中添加元素的操作会被阻塞,直到队列有空闲空间。
-
LinkedBlockingQueue:基于链表实现的阻塞队列,可以选择有界或无界。如果不指定容量,默认为无界队列。在队列为空时,尝试从队列中取出元素的操作会被阻塞,直到队列不为空;在队列已满时,尝试向队列中添加元素的操作会被阻塞,直到队列有空闲空间。
-
PriorityBlockingQueue:基于优先级堆实现的无界阻塞队列,元素按照优先级顺序进行排序。在队列为空时,尝试从队列中取出元素的操作会被阻塞,直到队列不为空;队列永远不会满,因为是无界队列。
-
DelayQueue:延迟队列,用于存放实现了 Delayed 接口的元素,可以设置元素的延迟时间。在元素的延迟时间还未到达时,尝试从队列中取出元素的操作会被阻塞,直到延迟时间到达。
-
SynchronousQueue:同步队列,不存储元素,用于线程间的直接交互。插入操作必须等待另一个线程的对应删除操作,反之亦然,因此称为同步队列。
这些阻塞队列提供了不同的特性和用途,可以根据具体需求选择合适的阻塞队列来使用。例如,对于有界队列需求可以选择 ArrayBlockingQueue 或 LinkedBlockingQueue;对于优先级排序需求可以选择 PriorityBlockingQueue;对于延迟处理需求可以选择 DelayQueue;对于直接线程间交互需求可以选择 SynchronousQueue。
75、什么是幂等性?
幂等性是指对同一操作的多次执行所产生的影响与执行一次的影响相同的性质。换句话说,无论对一个操作执行多少次,其结果都是一致的,不会产生副作用或者重复执行带来不同的结果。
在计算机科学和网络通信中,幂等性是一个非常重要的概念,特别是在分布式系统和网络通信中。一些常见的幂等性场景包括:
-
HTTP 请求:对于幂等性的 HTTP 请求,无论请求被重复发送多少次,服务器的状态都不会发生改变。例如,GET 请求通常是幂等的,而 POST 请求则不是幂等的。
-
数据库操作:对于幂等性的数据库操作,多次执行同一个操作不会导致数据库状态发生变化。例如,向数据库中插入一条记录,如果多次执行插入操作,最终结果应该是相同的。
-
消息队列消费:对于幂等性的消息消费,多次消费同一条消息不会产生额外的影响。这对于确保消息处理的准确性和可靠性非常重要。
-
支付系统:在支付系统中,保证支付请求的幂等性意味着多次提交相同的支付请求,只会产生一次支付操作,避免重复支付的问题。
实现幂等性的关键是要设计系统或接口,使得对同一个操作的多次执行不会产生不同的结果或产生副作用。通常可以通过唯一标识符、请求参数的幂等性设计、接口设计等方式来实现幂等性。保持接口的幂等性可以提高系统的可靠性和健壮性,防止因为重复执行而导致的错误或异常。
__EOF__

本文链接:https://www.cnblogs.com/MLYR/p/18252450.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)