提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
多线程相关-2
一、列举Java中创建线程的几种方式,并简述它们的不同之处。
线程创建补充
1.继承Thread类 重写run方法 创建对象并调用start方法
2.实现Runable接口 重写run方法 创建对象,创建线程对象,并将接口实现对象作为线程对象构造方法的参数传递,调用线程对象的start方法
3.实现Callable接口 重写call方法 创建接口实现类对象,使用FutureTask包装对象,最后使用Thread包装FutureTask类,调用线程对象的start方法
call方法有返回值,通过FutureTask对象的get方法获取
Runnable和Callable接口的不同在于Callable中有返回值,可抛出异常
第三种方法示例如下
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableFutureTask {
public static void main(String[] args) {
Callable<Integer> callable = () -> {
return 1 + 1;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
// 可以选择将FutureTask提交给线程池
// ExecutorService executorService = Executors.newFixedThreadPool(3);
// executorService.execute(futureTask);
// 或者直接用新线程启动
new Thread(futureTask).start();
try {
Integer result = futureTask.get();
System.out.println("Result: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出结果:
Result: 2
二、Callable和Future接口
阐述Callable接口与Future接口在Java多线程编程中的作用以及它们之间的关系。
Callable接口代表可能会产生结果或抛出异常的任务。跟Runnable相比,Callable接口有返回值并可以抛出异常
Future接口代表一个可能还没有完成的异步计算的结果。它提供了检查计算是否完成的方法以及等待计算完成获取计算结果的方法。Future还允许取消任务,以及在计算完成时接收通知。
当将一个Callable任务交给线程池时,会返回一个Future对象,代表任务执行结果。
二者合作执行流程如下:
1.提交任务:创建callable接口实现类,提交给线程池ExecuteService
2.获取Future对象:提交任务后线程池返回一个Future对象
3.等待并获取结果:调用Future对象的get方法等待任务完成并获取结果
4.任务取消:可以使用Future对象的cancel方法取消任务
5.异常处理:如果Callable接口执行抛出异常,get方法将会获取抛出的异常
示例代码:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class CallableFutureExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
Callable<Integer> callable = () -> {
// 模拟一些计算
return 1 + 1;
};
// 提交Callable任务并获取Future对象
Future<Integer> future = executorService.submit(callable);
try {
// 调用get()方法会阻塞当前线程,直到任务执行完成
Integer result = future.get();
System.out.println("Task result: " + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 关闭线程池
executorService.shutdown();
}
}
}
三、Future和FutureTask的区别
Future
定义:Future是一个接口,它代表了一个可能还没有完成的异步计算的结果。
作用:它提供了检查计算是否完成的方法,等待计算完成的方法,以及获取计算结果的方法(如果计算已经完成)。
实现:Future接口通常由线程池的submit或invoke方法返回,例如ExecutorService。
状态:Future接口本身并不包含计算的状态,它只是提供了一种机制来查询和获取异步执行任务的结果。
可取消性:Future接口提供了一个方法来取消任务,但是具体的取消行为取决于其实现。
FutureTask
定义:FutureTask是一个类,它是Future接口的一个具体实现,同时也实现了Runnable接口。
作用:它既可以作为任务被线程池执行,也可以作为结果的容器,用于检索计算结果。
实现:FutureTask可以手工创建并传给线程执行,或者作为线程池提交任务的结果被返回。
状态:FutureTask内部维护了任务的状态,包括计算是否开始、是否完成以及计算结果或异常。
可取消性:FutureTask实现了Future接口的取消方法,它允许在任务尚未开始执行时取消任务。
可运行性:由于实现了Runnable接口,FutureTask对象可以直接在任何线程上执行,也可以作为参数传递给Thread的构造器。
总结
Future是一个接口,提供了对异步计算结果的访问和控制。
FutureTask是Future接口的一个实现,它不仅可以作为异步计算的结果持有者,还可以作为一个任务被线程执行。
FutureTask提供了更灵活的使用方式,因为它可以手动启动,并且可以被线程池使用。
在实际应用中,通常不需要手动实现Future接口,而是直接使用FutureTask或线程池提供的Future对象。
四、解释synchronized和java.util.concurrent包中的锁在多线程同步中的区别和使用场景。
synchronized
内置锁:java关键字,实现方法或代码块同步,是一种内置锁或监视器锁
简单易用:使用简单,只需在方法声明上添加关键字或者在代码块中声明即可,不需要显式的释放锁
原子性:同一时刻只有一个线程执行代码块,保证了原子性
可见性:线程对共享变量的修改对其他线程可见,保证了可见性
死锁:不当使用可能会导致死锁,因为它不支持响应中断或超时
公平性:不支持设置公平性,即等待时间最长的线程不一定总能获得锁
java.util.concurrent 包中的锁
锁接口:包中提供了一些锁接口如Lock,ReadWriteLock等 ,通过具体类如ReentrantLock实现
高级特性:提供了比synchronized更高级的特性,如可响应中断、可设置超时、可设置公平等
灵活性:包中的锁更加灵活,可以根据需要选择不同的锁类型
复杂性:使用比synchronized更复杂,需要显式的释放锁
模板方法:提供了模板方法,如Executor框架,内部使用了高级锁,简化了并发编程
锁策略:提供了不同的锁策略,如读写锁(ReadWriteLock)可以更好地处理读多写少的场景
使用场景
synchronized
当同步代码块或方法的粒度较小,且不需要负责的锁策略时
在没有复杂的阻塞,中断或者超时需求时
作为一种快速简单的同步手段,适用于简单的同步问题
java.util.concurrent 包中的锁:
当需要更细粒度的锁控制,如读写分离时,可以使用 ReadWriteLock。
当需要可响应中断的同步,或者需要设置锁等待超时时,可以使用 Lock 接口的实现类,如 ReentrantLock。
在复杂的并发场景下,如需要条件对象(Condition)来控制多个线程的协调时。
当需要公平性控制,即希望等待时间最长的线程优先获取锁时。
总的来说,synchronized 适用于简单的线程同步需求,而 java.util.concurrent 包中的锁适用于更复杂的并发控制需求
五、死锁
描述死锁的四个必要条件,如何避免死锁。
死锁是在多线程环境下,线程之间相互等待其他线程持有的资源导致无法执行的一种状态
死锁的四个必要条件:
1.互斥条件:一个资源要么被线程持有,要么空闲,一个资源只能被一个线程占有
2.占有和等待条件:至少一个线程占有至少一个资源并等待其他线程占有的资源
3.不可抢占条件:资源只能由占有的线程资源释放,其他线程不能强制抢占
4.循环等待条件:存在一个线程-资源的循环等待链,每一个线程都在等待下一个线程持有的资源
如何避免死锁:
1.破坏占有等待条件:占有资源的线程不能等待其他线程的资源,可以通过一次性获取所有资源实现
2.破坏不可抢占条件:允许线程在等待时间过长时释放已占有的资源
3.破坏循环等待条件:为线程获取资源规定一个顺序,线程按照顺序获取资源
4.使用定时锁:使用ReentrantLock中的tryLock()或超时的lock()方法避免线程无限等待锁
5.使用死锁检验算法:使用死锁检验算法并在检验到死锁是进行一定的处理
6.避免使用嵌套锁:避免使用嵌套锁,或规定嵌套锁的顺序
7.使用juc包中提供的锁和同步机制:包中提供了更高级的锁机制,有助于避免死锁
六、线程池
线程池通过复用已经创建的线程处理新的任务达到减少线程创建和销毁的性能消耗的目的。线程池提高了程序的响应速度并且提供了更好的系统资源管理。
线程池工作原理:
任务提交:当一个任务(如实现了 Runnable 的对象)被提交给线程池时,线程池会根据当前的状态和配置参数来决定如何安排这个任务的执行。
核心线程:线程池至少会创建 corePoolSize 个核心线程,这些线程会一直存在,即使它们处于空闲状态,除非线程池被关闭。
非核心线程:当所有核心线程都在忙碌且有新的任务到来时,线程池会创建新的非核心线程来处理任务,直到达到 maximumPoolSize 定义的最大线程数量。
任务队列:如果线程池中的线程都在忙碌,而不能立即执行新的任务,那么这些任务会被放入一个阻塞队列(BlockingQueue)中等待。
线程复用:线程池会尝试复用线程,当一个线程完成一个任务后,它会去执行队列中的下一个任务。
线程终止:如果线程池中的线程数量超过了 corePoolSize,并且非核心线程空闲时间超过 keepAliveTime,则这些非核心线程会被终止。
关闭线程池:线程池提供了关闭操作,当调用 shutdown() 方法时,线程池会停止接受新任务,但会处理完阻塞队列中的任务。调用 shutdownNow() 会尝试立即停止所有正在执行的任务,并清空任务队列。
主要参数:
corePoolSize:线程池中始终保持的线程数量,即使它们处于空闲状态。
maximumPoolSize:线程池中允许的最大线程数量。
keepAliveTime:当线程池中正在运行的线程数量超过了 corePoolSize 时,多余的空闲线程能等待新任务的最长时间。超过这个时间后,多余的线程将被终止。
unit:与 keepAliveTime 参数配套使用的单位,可以是毫秒、秒、分钟等。
workQueue:一个阻塞队列,用于存放待执行的任务。
threadFactory:用于创建新线程的工厂,可以自定义线程的创建过程。
handler:拒绝策略,当任务太多,无法被线程池及时处理时,采取的策略,如丢弃任务或抛出异常。
七、 线程安全
如何确保一个类是线程安全的?请给出一些实现线程安全的方法。
1.使用synchronized同步方法或者代码块
2.使用ReentrantLock加锁实现同步
3.volatile关键字:对于需要保证可见性但不需要保证原子性的变量可以使用volatile关键字
4.不变类 确保类初始化后不可变 如String
5.原子类:使用AtomicInteger等原子类保证操作的原子性
6.线程局部存储:使用ThreadLocal存储线程独有数据,避免多线程间的变量共享
八、volatile关键字
解释Java中volatile关键字的作用,以及它在多线程环境下如何保证内存的可见性。
volatile 关键字的作用:
可见性:当一个变量被声明为 volatile,编译器和运行时都会注意到这个变量是共享的。线程在访问 volatile 变量时,都会去主内存中读取,而不是从线程的本地内存(如CPU缓存)读取。因此,对 volatile 变量的写操作对其他线程是可见的。
禁止指令重排:volatile 关键字还确保了与该变量相关的读写操作在执行时的顺序性。它禁止了特定类型的编译器和处理器重排序,确保在读操作之前,该变量的所有写操作已经完成。
轻量级锁:在某些情况下,volatile 可以作为一种轻量级锁来使用,尤其是在只涉及单个共享资源并且不需要原子性操作的场景下。
内存屏障:在读写 volatile 变量时,JVM会在读写操作前后自动插入内存屏障(memory barrier),确保在该点之前的所有操作对其他线程可见。
volatile 如何保证内存的可见性:
在多线程环境下,为了保持内存的一致性和可见性,JVM采用内存模型(Java Memory Model, JMM)来规定多线程环境下的内存一致性行为。
volatile 变量的读写遵循以下规则:
写操作:当一个线程向 volatile 变量写入数据时,JVM立即将该线程对应的本地内存中该变量的值刷新回主内存。
读操作:当线程读取 volatile 变量时,JVM立即从主内存中读取该变量的最新值到线程的本地内存中。
内存屏障:在读或写 volatile 变量时,JVM会在读写操作前后插入内存屏障,确保在读之前,所有依赖于该变量的读写操作都已完成。
原子性:尽管 volatile 可以保证可见性,但它不保证复合操作的原子性。例如,自增操作(i++)即使对 volatile 变量而言,也不是原子的。
线程调度:操作系统调度线程时,会考虑内存中变量的值。当线程读取 volatile 变量时,它会被告知需要从内存中读取该值,而不是使用缓存中的副本。
通过上述机制,volatile 确保了在多线程环境中,对共享变量的读写操作能够及时反映到所有线程中,从而维护了内存的一致性和可见性。然而,volatile 并不是解决所有并发问题的万能钥匙,它主要适用于状态标记等简单场景。对于更复杂的并发控制,可能需要使用锁或其他同步机制。
九、 线程通信
描述Java线程间通信的几种方式,比如通过wait()、notify()、notifyAll()方法。
1.通过共享进程变量进行通信
2.object 中提供的wait notify notifyAll方法
- wait() 线程释放锁,进入阻塞态,等待唤醒
- notify() 唤醒等待获取对象锁的随机线程
- notifyAll() 唤醒等待获取对象锁的所有线程
3.使用juc包中的Lock或者Condition进行线程通信
- Lock接口中提供了比synchronized关键字更高级的特性,如可中断、超时、公平性
- Condition接口中提供 await() signal() signalAll()
作用与Object中的wait() notify() notifyAll()方法作用相对应
区别:Condition中的代码依赖于Lock接口,即需要在lock()和unlock()中的代码使用
4.volatile关键字
volatile关键字可以保证变量的可见性,使得线程对变量的修改对所有的线程可见。但无法保证操作原子性
5.juc包中的BlockingQueue接口实现了一个线程安全的队列,是生产者-消费者模型的解决方案
当向队列中添加元素时,若队列已满,则生产者线程阻塞
当从队列中获取元素时,若队列为空,则消费者线程阻塞
十、并发集合
列举Java并发包中提供的并发集合类,并简述它们相较于传统集合的优势。
- ConcurrentHashMap:线程安全的HashMap,ConcurrentHashMap允许多个线程同时读写,而不需要通过外部同步。
- BlockingQueue:线程安全的队列接口,使用ReentranLock锁和阻塞机制实现线程安全,当队列为空,阻塞消费者线程,当队列已满,阻塞生产者线程
- CopyOnWriteArrayList:线程安全的变长数组,每次写操作都复制一份出来并在复制体上进行操作,读操作读取原来的数组,适用于读多写少的场景