目录
- 什么是线程?什么是进程?为什么要有线程?有什么关系与区别?
- 什么是守护线程?
- 如何创建、启动 Java 线程?
- 线程池参数
- 详细解释Callable接口和Future类
- 偏向锁 / 轻量级锁 / 重量级锁
- synchronized 和 java.util.concurrent.lock.Lock 之间的区别
- synchronized 和 java.util.concurrent.lock.Lock 之间的区别
- java.util.concurrent.lock.Lock 与 java.util.concurrent.lock.ReadWriteLock 之间的区别
- 什么是死锁?发生的条件,避免死锁
- 介绍一下ForkJoinPool的使用
什么是线程?什么是进程?为什么要有线程?有什么关系与区别?
线程:
线程是进程中的最小执行单元,是CPU调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的资源,包括内存空间、文件描述符等。线程之间可以并发执行,使得程序可以同时处理多个任务,提高了系统的响应能力和并发性。
进程:
进程是操作系统中的一个执行实例,是程序在计算机上的一次执行活动。每个进程都有自己独立的内存空间和系统资源,进程之间是相互独立的。进程的切换代价相对较高,因为需要保存和恢复进程的所有状态信息。
为什么要有线程?
在单核CPU时代,进程是独立运行的,每个进程有自己的地址空间,进程之间切换开销大。为了充分利用CPU的计算资源,提高系统并发能力,引入了线程的概念。线程在同一个进程内共享进程的资源,线程间切换开销较小,可以实现更高效的多任务处理。
关系与区别:
- 关系:线程是进程的一部分,一个进程可以包含多个线程。线程共享进程的资源,包括内存空间、文件描述符等。
- 区别:进程是一个独立的执行实例,有自己独立的内存空间和系统资源。线程是进程的最小执行单元,多个线程共享进程的资源。进程之间相互独立,线程之间可以并发执行,共享进程的资源。
总结:线程是进程内部的执行流,是处理器执行任务的最小单位。一个进程可以包含多个线程,线程之间可以并发执行,共享进程的资源,从而提高系统的并发性和性能。多线程编程可以实现更高效的多任务处理,但也需要注意线程同步和共享资源的安全性问题。
什么是守护线程?
守护线程(Daemon Thread)是一种特殊类型的线程,其特点是在程序运行时在后台提供一种通用服务的功能。与普通线程(用户线程)相对应,守护线程的生命周期依赖于程序中是否还存在正在运行的用户线程。当所有的用户线程结束运行时,守护线程会被自动终止,而不会等待其运行完成。
在Java中,通过调用Thread类的setDaemon(true)
方法将线程设置为守护线程。守护线程通常用于提供程序的支持和后台服务,如垃圾回收器(GC线程)就是一个典型的守护线程。垃圾回收器在程序运行过程中自动回收不再使用的对象,但当所有用户线程执行完毕时,程序就终止了,此时也不需要再继续执行垃圾回收的工作,因此垃圾回收线程会自动终止。
守护线程的一个重要应用场景是Web服务器,它可以创建一个守护线程来处理网络连接,如果所有的用户请求线程都结束了,服务器就没有处理请求的必要了,守护线程会自动退出。
需要注意的是,守护线程并不适合处理需要完整执行的任务,因为它的生命周期是不可控的,当所有用户线程结束时,它会被强制终止,可能导致任务未完成。因此,守护线程适合处理后台服务和支持性工作,而不适合处理关键业务逻辑。
如何创建、启动 Java 线程?
在Java中,创建和启动线程通常有两种方式:继承Thread类和实现Runnable接口。
1. 继承Thread类:
创建一个继承自Thread类的子类,并重写run()方法来定义线程的执行逻辑。然后通过调用start()方法来启动线程。示例代码如下:
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行逻辑
System.out.println("Thread is running!");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
2. 实现Runnable接口:
创建一个实现了Runnable接口的类,并实现run()方法来定义线程的执行逻辑。然后将实现了Runnable接口的类作为参数传递给Thread类的构造方法,并调用start()方法启动线程。示例代码如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行逻辑
System.out.println("Thread is running!");
}
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start(); // 启动线程
}
}
无论是继承Thread类还是实现Runnable接口,调用start()方法后,线程会进入就绪状态,并由Java虚拟机自动调度执行。需要注意的是,线程的执行顺序是由系统的线程调度器决定的,因此线程的执行顺序可能是随机的。
除了继承Thread类和实现Runnable接口外,还可以使用Callable接口和Future类创建线程。Callable接口允许线程返回一个结果,而Future类可以用于获取Callable线程的返回值。这是一种更加灵活的方式来创建和管理线程。
线程池参数
在Java中,线程池的参数主要由ThreadPoolExecutor
类的构造方法来定义,它的构造方法有以下几个参数:
-
corePoolSize:
表示线程池的核心线程数,也就是线程池中始终保持活动状态的线程数量。即使这些线程处于空闲状态,也不会被回收。当有新的任务提交时,如果核心线程数还没有达到上限,会优先创建新的核心线程来执行任务。 -
maximumPoolSize:
表示线程池的最大线程数,包括核心线程和非核心线程。当有新的任务提交时,如果核心线程数已满,并且线程池中的线程数量还没有达到最大线程数,会创建新的非核心线程来执行任务。当线程池中的线程数达到最大线程数后,如果任务继续提交,任务会被放入任务队列中等待执行。 -
keepAliveTime:
表示非核心线程的空闲时间。当非核心线程空闲时间超过该值时,会被回收释放,以减少线程池的线程数量。这个参数只有在allowCoreThreadTimeOut
设置为true
时才生效。 -
unit:
表示keepAliveTime的时间单位,可以是TimeUnit.SECONDS
、TimeUnit.MINUTES
等等。 -
workQueue:
表示任务队列,用于存放等待执行的任务。当线程池的线程数达到核心线程数后,后续提交的任务会被放入任务队列中等待执行。线程池提供了多种任务队列的实现,常用的有ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
等。 -
threadFactory:
表示线程工厂,用于创建新的线程。可以通过自定义线程工厂来给线程设置特定的名称、优先级等。 -
handler:
表示线程池的拒绝策略,当线程池的任务队列和线程数都已满,无法继续接收新的任务时,会根据指定的拒绝策略来处理这些任务。常用的拒绝策略有ThreadPoolExecutor.AbortPolicy
(抛出异常)、ThreadPoolExecutor.CallerRunsPolicy
(由提交任务的线程来执行任务)、ThreadPoolExecutor.DiscardPolicy
(直接丢弃任务)等。
综上所述,线程池的参数主要包括核心线程数、最大线程数、非核心线程的空闲时间、任务队列、线程工厂和拒绝策略等。合理地设置这些参数可以根据系统的需求来控制线程池的并发度、资源消耗和任务调度,以达到最优的性能和资源利用率。
详细解释Callable接口和Future类
Callable
接口和Future
类是Java多线程中用于处理有返回值的任务的一组接口和类。它们通常与线程池结合使用,使得多线程编程更加灵活和高效。
Callable接口:
Callable
接口是一个泛型接口,定义了一个带有返回值的任务,它只有一个方法call()
,没有像Runnable
接口那样的run()
方法。call()
方法可以返回一个结果,并且可以抛出异常。通常情况下,我们可以通过实现Callable
接口来创建具有返回值的任务,并将任务提交给线程池执行。Callable
接口的定义如下:
public interface Callable<V> {
V call() throws Exception;
}
Future类:
Future
类是一个接口,它代表了异步计算的结果。当一个线程提交了一个Callable
任务到线程池后,线程池会返回一个Future
对象,通过该对象可以获得任务的执行结果。Future
接口定义了一些方法,用于查询任务是否完成、获取任务的执行结果或者取消任务的执行。常用的Future
实现类是FutureTask
。Future
接口的定义如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
通过Future
对象,我们可以在主线程中继续执行其他任务,然后在需要获取任务结果的地方调用get()
方法来等待任务执行完成并获取结果。如果任务还没有执行完成,get()
方法将会阻塞主线程,直到任务执行完成并返回结果。如果不想等待任务执行完成,可以使用isDone()
方法来判断任务是否完成。
总结:Callable
接口用于定义带有返回值的任务,Future
类用于获取任务的执行结果或者取消任务的执行。通过这两个接口,我们可以更好地管理多线程任务,实现高效的并发编程。
偏向锁 / 轻量级锁 / 重量级锁
偏向锁、轻量级锁、重量级锁都是Java中用于优化锁的实现,针对不同的锁竞争场景,采用不同的策略来提高锁的性能。
1. 偏向锁(Biased Locking):
偏向锁是Java中针对单线程访问同步块的优化措施。它的设计思想是假设在大多数情况下,锁只会被一个线程访问,因此当一个线程访问同步块时,会将对象头中的标记设置为偏向锁,并记录下持有偏向锁的线程ID。这样,下次这个线程再次访问同步块时,就无需再进行加锁操作,而是直接进入临界区。如果有其他线程尝试竞争偏向锁,偏向锁就会升级为轻量级锁。
2. 轻量级锁(Lightweight Locking):
轻量级锁是针对多个线程交替执行同步块的优化措施。当一个线程尝试获取锁时,会先在栈帧中分配一块用于存储锁记录的空间,并将对象头中的标记设置为轻量级锁。然后线程尝试使用CAS(比较并交换)操作来尝试获取锁,如果获取成功,则进入临界区执行。如果CAS操作失败,说明有其他线程也在竞争锁,这时轻量级锁会膨胀为重量级锁。
3. 重量级锁(Heavyweight Locking):
重量级锁是Java中默认的锁实现,用于处理复杂的锁竞争场景。当一个线程尝试获取锁时,会进入重量级锁的阻塞状态,操作系统会将该线程挂起,直到锁被释放,然后唤醒该线程。重量级锁采用操作系统的原生锁机制,会涉及用户态和内核态的切换,开销较大,适用于竞争激烈的情况。
在锁的优化中,JVM会根据线程竞争的情况自动选择偏向锁、轻量级锁或重量级锁来提高锁的性能。偏向锁适用于只有一个线程访问同步块的情况,轻量级锁适用于多个线程交替执行同步块的情况,而重量级锁适用于竞争激烈的情况。这些优化措施都是为了减少锁的开销,提高程序的并发性能。
synchronized 和 java.util.concurrent.lock.Lock 之间的区别
synchronized
和java.util.concurrent.lock.Lock
都是Java中用于实现线程同步的机制,但它们之间有一些重要的区别:
-
锁的类型:
synchronized
是Java中的关键字,属于内置锁(Intrinsic Lock),可以直接在方法或代码块中使用。每个Java对象都有一个内置锁,通过synchronized
关键字来获取。java.util.concurrent.lock.Lock
是Java并发包中提供的接口,属于显式锁(Explicit Lock)。它提供了更灵活的锁定和释放机制,并且可以有多个条件变量。
-
获取锁的方式:
synchronized
的获取锁是隐式的,在进入synchronized
代码块或方法时自动获取锁,在退出代码块或方法时自动释放锁。java.util.concurrent.lock.Lock
的获取锁是显式的,需要手动调用lock()
方法来获取锁,调用unlock()
方法来释放锁。这样可以更精确地控制锁的范围和持有时间。
-
锁的可中断性:
synchronized
获取锁的过程是不可中断的,即使其他线程尝试中断持有锁的线程,也无法中断。java.util.concurrent.lock.Lock
中的锁可以通过lockInterruptibly()
方法实现可中断的获取锁操作,当其他线程中断当前线程时,可以中断获取锁的过程。
-
锁的公平性:
synchronized
锁是非公平的,当一个线程释放锁后,任何一个等待该锁的线程都有机会获取锁。java.util.concurrent.lock.Lock
中的锁可以是公平的,通过ReentrantLock
类的构造函数可以指定是否使用公平锁,默认是非公平锁。
-
灵活性和功能:
java.util.concurrent.lock.Lock
提供了更多的功能,例如可以尝试获取锁、设定获取锁的超时时间、创建多个条件变量等,可以满足更复杂的同步需求。synchronized
相对简单,适用于一些简单的同步需求,而且在JVM中使用synchronized
的优化措施,如偏向锁、轻量级锁等,可以带来一定的性能优势。
总体而言,对于简单的同步需求,synchronized
是较为方便的选择。而对于复杂的同步需求,或者需要更精细地控制锁的行为,java.util.concurrent.lock.Lock
提供了更多的灵活性和功能,可以更好地满足需求。
synchronized 和 java.util.concurrent.lock.Lock 之间的区别
synchronized
和java.util.concurrent.lock.Lock
都是Java中用于实现线程同步的机制,但它们之间有一些重要的区别:
-
锁的类型:
synchronized
是Java中的关键字,属于内置锁(Intrinsic Lock),可以直接在方法或代码块中使用。每个Java对象都有一个内置锁,通过synchronized
关键字来获取。java.util.concurrent.lock.Lock
是Java并发包中提供的接口,属于显式锁(Explicit Lock)。它提供了更灵活的锁定和释放机制,并且可以有多个条件变量。
-
获取锁的方式:
synchronized
的获取锁是隐式的,在进入synchronized
代码块或方法时自动获取锁,在退出代码块或方法时自动释放锁。java.util.concurrent.lock.Lock
的获取锁是显式的,需要手动调用lock()
方法来获取锁,调用unlock()
方法来释放锁。这样可以更精确地控制锁的范围和持有时间。
-
锁的可中断性:
synchronized
获取锁的过程是不可中断的,即使其他线程尝试中断持有锁的线程,也无法中断。java.util.concurrent.lock.Lock
中的锁可以通过lockInterruptibly()
方法实现可中断的获取锁操作,当其他线程中断当前线程时,可以中断获取锁的过程。
-
锁的公平性:
synchronized
锁是非公平的,当一个线程释放锁后,任何一个等待该锁的线程都有机会获取锁。java.util.concurrent.lock.Lock
中的锁可以是公平的,通过ReentrantLock
类的构造函数可以指定是否使用公平锁,默认是非公平锁。
-
灵活性和功能:
java.util.concurrent.lock.Lock
提供了更多的功能,例如可以尝试获取锁、设定获取锁的超时时间、创建多个条件变量等,可以满足更复杂的同步需求。synchronized
相对简单,适用于一些简单的同步需求,而且在JVM中使用synchronized
的优化措施,如偏向锁、轻量级锁等,可以带来一定的性能优势。
总体而言,对于简单的同步需求,synchronized
是较为方便的选择。而对于复杂的同步需求,或者需要更精细地控制锁的行为,java.util.concurrent.lock.Lock
提供了更多的灵活性和功能,可以更好地满足需求。
java.util.concurrent.lock.Lock 与 java.util.concurrent.lock.ReadWriteLock 之间的区别
java.util.concurrent.lock.Lock
和java.util.concurrent.lock.ReadWriteLock
都是Java并发包中用于实现线程同步的接口,它们之间的区别主要在于锁的类型和用途:
1. 锁的类型:
-
java.util.concurrent.lock.Lock
是一种通用的锁接口,用于实现基本的互斥锁。它提供了两个基本方法:lock()
用于获取锁,unlock()
用于释放锁。Lock
接口的实现类ReentrantLock
是一种可重入锁,它允许同一个线程多次获取同一个锁,避免了死锁的问题。 -
java.util.concurrent.lock.ReadWriteLock
是一种读写锁接口,它可以让多个线程同时读取共享资源,但在写操作时需要互斥。ReadWriteLock
接口提供了两个锁:读锁(读共享锁)和写锁(写独占锁)。读锁可以被多个线程同时获取,用于读取共享资源;而写锁只能被一个线程获取,用于修改共享资源。
2. 用途:
-
java.util.concurrent.lock.Lock
适用于一般性的互斥场景,它提供了灵活的锁定和解锁机制,可以手动控制锁的获取和释放。它的实现类ReentrantLock
可以实现公平锁或非公平锁,也支持可中断的获取锁操作。 -
java.util.concurrent.lock.ReadWriteLock
适用于读多写少的场景,它可以提高读操作的并发性能。在读多写少的情况下,多个线程可以同时获取读锁来读取共享资源,而只有在写操作时需要排他性,此时才需要获取写锁。
综上所述,Lock
接口适用于通用的互斥场景,提供了灵活的锁定和解锁机制;而ReadWriteLock
接口适用于读多写少的场景,可以提高读操作的并发性能。在不同的场景下,可以根据需求选择合适的锁实现。
什么是死锁?发生的条件,避免死锁
死锁发生的条件
互斥,共享资源只能被一个线程占用
占有且等待,线程 t1 已经取得共享资源 s1,尝试获取共享资源 s2 的时候,不释放共享资源 s1
不可抢占,其他线程不能强行抢占线程 t1 占有的资源 s1
循环等待,线程 t1 等待线程 t2 占有的资源,线程 t2 等待线程 t1 占有的资源
避免死锁的方法
对于以上 4 个条件,只要破坏其中一个条件,就可以避免死锁的发生。
对于第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥。
其他三个条件,我们可以尝试
一次性申请所有的资源,破坏 “占有且等待” 条件
占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件
按序申请资源,破坏 “循环等待” 条件