206道Java面试题(28~46)
-
28.Array和ArrayList有什么区别?
一、基本性质
Array(数组)-
Array是一种固定大小的数据结构。
-
用于存储多个相同类型的元素。
-
创建时需要指定数组的大小,且长度定义完后不能改变。
ArrayList(动态数组)
-
ArrayList是Java Collections框架的一部分。
-
长度可变的集合类。
-
可以根据需要自动改变其大小。
二、存储内容
Array-
基本数据类型和对象类型。
-
但是一个数组就只能存储一种数据类型。
ArrayList
-
只能存储对象类型。
-
若想要存储基本数据类型,就必须使用相应的封装类。
三、维度
Array-
可以是多维的,如:二维,三维数组等。
-
可以创建特殊的数据结构,如矩阵。
ArrayList
-
只能是一维的。
-
无法直接创建多维ArrayList。
四、方法和属性
Array-
数组本身不具有方法,通常需要通过循环或其他方式对数组进行操作.
-
提供一个length属性,表示数组可以存储的元素个数(即数组的长度).
ArrayList
-
提供了丰富的方法,如add()、remove()、get()、size()等,方便对元素进行操作和管理。
-
提供一个size()方法,返回ArrayList当前存储的元素个数。
五、性能和灵活性
Array:- 在获取或设置元素时性能较高,因为是通过索引直接访问。
- 由于大小固定,所以在添加或删除元素时需要创建新的数组并复制元素,性能可能稍慢。
ArrayList:
- 在添加或删除元素时会自动调整其内部数组的大小,可能会涉及到数组的复制和移动,但在大多数情况下性能仍然较高。
- 提供了更高的灵活性,因为可以动态地增加或减少元素。
-
-
29.在Queue中poll()和remove()有什么区别?
在Java的
Queue
接口中,poll()
和remove()
方法都用于从队列中移除并返回头部元素,
1、返回值的处理-
如果队列为空,
poll()
方法会返回null
。这意味着在调用poll()
时,不需要进行额外的空检查,因为null
返回值本身就是一个明确的信号,表明队列中没有元素可供移除。 -
如果队列为空,,remove()方法会抛出NoSuchElementException异常。在使用此方法前需要确认队列不为空,或者使用try-catch来捕获异常。
2、异常的处理
-
poll()方法返回值为null不是抛出异常,所以更加安全。特别是处理可能为空的队列时。
-
remove()方法通过抛出异常来强制调用者注意队列的状态,这在某些情况下可能是有用的,因为它可以更早地揭示潜在的错误条件。
3、使用的场景
-
poll()方法更适于不确定队列是否为空的情况进行元素移除。不需要另外进行空检查或异常处理。
-
remove()方法使用于移除元素时数组总是为非空的情况。队列为空抛出异常是一种更好的错误处理策略。可以中断程序执行,检查列表为空的原因。
4、一致性
Queue
接口中的remove()
方法的行为与Collection
接口中的remove()
方法的行为略有不同。在Collection
接口中,remove()
方法用于移除指定元素(如果存在),并返回true
(如果集合包含指定的元素)。然而,在Queue
接口中,为了保持一致性,remove()
方法被重载以移除并返回队列头部的元素(如果队列不为空)。这可能会导致一些混淆,特别是在同时处理Queue
和Collection
类型的代码时。
-
-
30.哪些集合类是线程安全的?
1、常见集合
2、什么是“集合线程不安全”
当多个并发同时对非线程安全例如:当多个线程访问同一个集合或Map时,如果有超过一个线程修改了ArrayList集合,则程序必须手动保证该集合的同步性。
3、线程安全和不安全的集合-
线程安全:Vector、HashTable、Properties是线程安全的。
-
线程不安全:ArrayList、Linked List、Hash Set、HashMap等都是线程不安全的。
-
线程安全,相应的效率较低。线程不安全的,效率相对较高。
4、综合考虑线程不安全和效率低
<T> Collection<T> synchronizedCollection(Collection<T> c); //返回指定collection 对应的线程安全的collection。 static <T> List<T> synchronizedList(List<T> list); //返回指定List对象对应的线程安全的List 对象。 static <K, V> Map<K, V> synchronizedMap(Map<K, V> m); //返回指定Map对象对应的线程安全的Map对象。 static <T> Set<T> synchronizedSet(Set<T> s); //返回指定Set对象对应的线程安全的Set对象。 static <K, V> SortedMap<K, V> synchronizedSortedMap(SortedMap<K, V> m); //返回指定SortedMap对象对应的线程安全的SortedMap对象。
-
-
31.迭代器Iterator是什么?
迭代器(Iterator)是一种程序设计中的软件设计模式,主要用于遍历容器对象(如链表、数组等)中的元素,而无需暴露容器的内部结构。
1、定义与概述
迭代器提供了一个统一的接口,允许开发人员逐个访问容器中的元素,而无需关心底层的数据存储和访问机制。通过迭代器,开发人员可以方便地遍历集合、数组或其他容器类型的数据结构。
2、主要的方法与功能-
HashNext():此方法用于检查迭代器是否还有下一个元素。如果迭代器仍有元素可供遍历,则返回true;否则返回false。
-
Next:此方法用于获取迭代器当前指向的下一个元素。如果迭代器没有更多元素,则调用此方法可能会抛出异常(如NoSuchElementException)。
-
remove():用于从底层集合中删除迭代器最近返回的元素。
-
-
32.IteraTor怎么使用?有什么特点?
1、使用方法
(1)获取迭代器- 通过调用集合的
iterator()
方法来获取一个Iterator对象。例如,在Java中,对于ArrayList
、HashSet
等集合,可以调用它们的iterator()
方法来获取相应的迭代器。
(2)遍历集合
-
使用
hasNext()
方法检查迭代器中是否还有元素。如果有,则进入循环。 -
在循环中,使用
next()
方法获取迭代器当前指向的下一个元素,并更新迭代器的状态(即指向下一个元素)。
2、特点
(1)顺序访问- Iterator提供了一种顺序访问集合中元素的方法,可以按照一定的顺序(如插入顺序、自然顺序等)逐个访问集合中的元素。
(2)通用性
- Iterator是Java集合框架中的一个通用接口,可以用于遍历各种不同类型的集合(如
ArrayList
、HashSet
、LinkedList
等),而无需关心底层的数据结构和访问方式。
(3)安全性
- 使用Iterator遍历集合时,可以避免直接使用索引访问可能导致的越界等问题。且Java还提供了元素删除的方法remove(),可以避免在遍历中直接修改集合而造成的异常。
(4)解耦
- Iterator模式通过将集合对象的遍历行为分离出来,抽象成迭代器类来实现,从而实现了集合与遍历算法的解耦。这使得可以在不修改集合本身的情况下,通过实现不同的迭代器类来支持不同的遍历策略。
(5)扩展性
- 由于Iterator是一个接口,因此可以通过实现该接口来创建自定义的迭代器类,以支持新的遍历策略或数据结构。这使得Iterator模式具有很好的扩展性。
3、案例
- 通过调用集合的
import java.util.ArrayList;
import java.util.Iterator;
public class IteratorExample {
public static void main(String[] args) {
// 创建一个ArrayList并添加元素
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
list.add("cherry");
// 获取迭代器
Iterator<String> iterator = list.iterator();
// 使用迭代器遍历集合
while (iterator.hasNext()) {
String fruit = iterator.next();
System.out.println(fruit);
}
}
}
-
33.Iterator和List Iterator有什么区别?
1、遍历方向
-
Iterator:只能单向遍历集合,即只能从前向后遍历。
-
ListIterator:支持双向遍历集合,既可以从前向后遍历,也可以从后向前遍历。
2、适用场景
-
Iterator:是一个通用迭代器接口,适用于所有实现了Iterable接口的集合,如ArrayList、HashSet等。
-
ListIterator:是Iterator接口的子接口,专门用于遍历List集合,如ArrayList、LinkedList等。因此,ListIterator只能用于List接口的集合。
3、元素修改能力
- Iterator:只能遍历集合,不能修改集合中的元素。虽然Iterator提供了remove()方法用于删除元素,但这并不属于修改元素的操作,而是从集合中移除当前迭代器指向的元素。
- ListIterator:不仅可以遍历集合,还可以修改集合中的元素。它提供了set()方法用于修改当前迭代器指向的元素的值。此外,ListIterator还提供了add()方法,可以在当前元素之前添加新元素。
4、索引支持
-
Iterator:不支持获取元素索引的方法。
-
ListIterator:提供了nextIndex()和previousIndex()方法,分别用于获取下一个元素和上一个元素的索引值。这使得在遍历过程中可以方便地获取元素的索引信息。
5、适用场景
-
Iterator:适用于只需要单向遍历集合不需要修改集合中元素的场景,且不用获取元素索引的场景。
-
ListIterator:适用于需要双向遍历,修改元素,获取元素索引的场景。
-
-
34.怎么确保一个集合不能被修改?
1、使用不可变的集合包装类
-
Collections.unmodifiableList(List<? extends T> list)
-
Collections.unmodifiableSet(Set<? extends T> set)
-
Collections.unmodifiableMap(Map<? extends K, ? extends V> m)
-
Collections.unmodifiableSortedMap(SortedMap<K, ? extends V> m)
-
Collections.unmodifiableNavigableMap(NavigableMap<K, ? extends V> m)
-
Collections.unmodifiableCollection(Collection<? extends T> c)
使用这些包装器后,任何尝试修改集合的操作都会抛出
ConcurrentModificationException
异常。List<String> mutableList = new ArrayList<>(); mutableList.add("a"); mutableList.add("b"); List<String> immutableList = Collections.unmodifiableList(mutableList); // 尝试修改不可变集合会抛出异常 // immutableList.add("c"); // 这将抛出ConcurrentModificationException
2、使用不可变的集合类
Java 9 引入了新的不可变集合类,如List.of()
,Set.of()
,Map.of()
等,它们直接返回不可变的集合实例。这些方法提供了更简洁的语法来创建不可变集合。List<String> immutableList = List.of("a", "b", "c"); // immutableList.add("d"); // 编译错误,因为List.of()返回的List是不可变的 Set<String> immutableSet = Set.of("a", "b", "c"); // immutableSet.add("d"); // 编译错误 Map<String, Integer> immutableMap = Map.of("a", 1, "b", 2); // immutableMap.put("c", 3); // 编译错误
3、使用自定义的不可变集合
Java虽然提供了丰富的不可变的集合类,但是在某些情况下,需要创建自定义的不可变集合类。这通常涉及到重写集合类的所有修改方法(如add()
,remove()
,set()
等),使它们抛出UnsupportedOperationException
或类似的异常。
4、通过封装和私有化集合的修改方法:
创建一个自定义的集合类,该类封装了一个私有的可变集合,并只提供读取操作的方法。这样,外部代码就无法直接修改集合。import java.util.*; public class ImmutableSetWrapper<T> { private final Set<T> set; public ImmutableSetWrapper(Collection<T> collection) { this.set = new HashSet<>(collection); } public boolean contains(T item) { return set.contains(item); } public int size() { return set.size(); } // 禁止其他修改操作 private void add(T item) { throw new UnsupportedOperationException("Cannot modify an ImmutableSetWrapper"); } // 可以根据需要添加更多只读方法 } // 使用 public class Main { public static void main(String[] args) { ImmutableSetWrapper<String> wrapper = new ImmutableSetWrapper<>(Arrays.asList("A", "B", "C")); // 读取操作有效 System.out.println(wrapper.contains("A")); // 输出 true // 尝试修改会抛出异常 // wrapper.add("D"); // UnsupportedOperationException } }
5、注意事项
- 当使用不可变集合包装器时,要注意原始集合仍然可以被修改。包装器只是提供了一个不可变的视图,而不是真正地将集合转换为不可变状态。
- 不可变集合的不可变性是浅层的。如果集合包含可变对象,这些对象本身仍然可以被修改。
- 不可变集合有助于提升线程安全性,因为它们不需要额外的同步来防止并发修改。
-
-
35.并行和并发有什么区别?
并发
并发是指在同一时间段内,多个任务或进程可以同时执行,但在宏观上仍然是“串行”的。这意味着,虽然多个任务看起来是同时进行的,但在单处理器系统中,它们实际上是轮流使用CPU资源的。在微观层面,并发表现为多个进程或线程快速交替执行,而不是真正的同时执行。并发编程的目标是充分利用处理器的每一个核,以达到最高的处理性能。在现代操作系统中,并发可以通过进程、线程或I/O多路复用来实现。
并行
并行则是指多个任务在同一时刻真正意义上的同时执行。这要求系统具有多个处理器核心,以便多个任务可以同时在不同的处理器核心上运行。在并行处理中,无论是从微观还是宏观上看,多个任务都是同时发生的。并行在微观和宏观上都是同时发生的,因为它涉及到多个处理器或核心同时处理多个任务。
区别总结
1、执行方式-
并发:多个任务在同一个时间段内交替执行,宏观上同时,微观上仍是顺序执行。
-
并行:多个任务在同一时刻同时执行,需要多个处理器核心支持。
2、处理器需求
-
并发:可以在单处理器或多处理器系统中存在,但单处理器系统中需要通过时间分片技术实现。
-
并行:要求有多个处理器核心,以便多个任务可以真正同时执行。
3、性能
-
并发:通过优化任务调度和资源分配来提高性能,但受限于单个处理器的处理能力。
-
并行:通过多个处理器核心同时执行任务来显著提高性能。
4、实现方式
-
并发:依赖于操作系统的cpu时间分片技术。
-
并行:需要实际的物理多处理器或多核处理器。
-
-
36.线程和进程的区别?
1、线程的概念
线程是进程中执行运算的最小单位。是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
好处-
(1)易于调度。
-
(2)提高并发性,通过线程可以方便有效的实现并发性。进程可创建多个线程来执行同一程序的不同部分。
-
(3)消耗小。创建线程比创建进程要快,所需的开销较少。
-
(4)有利于充分发挥多处理器的功能。通过创建多线程进程,每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。
2、线程与进程
-
进程: 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1–n个线程。(进程是资源分配的最小单位)
-
线程: 同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位)
-
线程和进程一样分为5个阶段:创建、就绪、运行、阻塞、终止。
-
多线程:在同一个程序中有多个顺序流在执行。
-
多进程:指操作系统能同时运行多个任务。
-
每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。
-
在Java中,一个应用程序可以包含多个线程。每个线程执行特定的任务,并可与其他线程并发执行多线程使系统的空转时间最少,提高CPU利用率、多线程编程环境用方便的模型隐藏CPU在任务间切换的事实在Java程序启动时,一个线程立刻运行,该线程通常称为程序的主线程。
-
主线程的重要体现:
-
1、他是产生其他子线程的线程。
-
通常他最后完成执行,因为他执行各种关闭动作。
-
3、进程与线程的区别
-
调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
-
并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
-
拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
-
系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
4、线程和进程的关系
-
一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
-
资源分配给进程,同一进程的所有线程共享该进程的所有资源。
-
处理机分给线程,即真正在处理机上运行的是线程。
-
-
37.守护线程是什么?
定义
守护线程是在程序运行过程中在后台进行的线程,它的任务是为其他线程提供服务和支持。当所有的非守护线程结束运行时,虚拟机会自动退出。在Java中,通过设置线程的setDaemon(true)方法将线程设置为守护线程。守护线程通常用来执行一些辅助性的任务,比如垃圾回收、内存管理等。
用途
多线程中的守护线程是一种特殊的线程,其作用是在主线程退出时,自动退出。守护线程通常用于执行一些后台任务,不需要和主线程同步进行。-
后台任务:如日志记录、系统监控、内存清理等。这些任务不是必须的,但是可以提供额外的功能和服务。
-
资源回收:在主线程退出时自动回收一些资源。如关闭打开的文件、释放占用的内存等。
-
服务线程:如网络监听、接收客户端请求等。可以保持服务持续运行且主线程退出也不会影响服务的正常工作。
生命周期
-
线程:
-
用户线程:创建的普通线程。
-
守护线程:创建的特殊线程。
-
-
创建:通过调用Thread类的setDaemon方法将线程设置为守护线程。守护线程必须在启动前设置,否则会抛出IllegalThreadStateException异常。
-
启动:通过调用Thread类的start方法启动线程。
-
运行:守护线程会在用户线程结束时自动退出。如果所有的用户线程都结束了,守护线程会自动停止。
-
终止:守护线程的终止条件有两种情况:一是所有的用户线程都结束了,二是守护线程自己调用了自己的interrupt方法。
注意:
守护线程不能用于释放资源或执行重要的操作,因为它可能在任何时候被终止。守护线程一般用于执行一些后台任务,如垃圾回收、日志记录等。public class DaemonThreadExample { public static void main(String[] args) { Thread daemonThread = new Thread(() -> { while (true) { System.out.println("Daemon thread is running"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); daemonThread.setDaemon(true); daemonThread.start(); try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Main thread is exiting"); } }
-
-
38.创建线程有哪几种方式?
- 实现Runnable接口
可以通过实现Runnable接口来创建线程,然后将实现了Runnable接口的对象作为参数传递给Thread类的构造方法。
class MyRunnable implements Runnable { public void run() { System.out.println("Thread is running..."); } } public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 } }
- 实现Callable接口
当我们需要创建一个可以返回结果的线程时,就可以使用实现了Callable接口的方式。
与Runnable接口相比,Callable接口的call()方法可以返回结果并抛出异常。import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; class MyCallable implements Callable<String> { public String call() { return "Thread is running..."; } } public class Main { public static void main(String[] args) { // 创建Callable实现类的实例 Callable<String> callable = new MyCallable(); // 创建FutureTask对象,用于包装Callable对象 FutureTask<String> futureTask = new FutureTask<>(callable); // 创建线程并启动 Thread thread = new Thread(futureTask); thread.start(); try { // 获取线程执行结果 String result = futureTask.get(); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
- 继承Thread类
通过继承Thread类来创建线程,然后重写run()方法来定义线程执行的任务。
class MyThread extends Thread { public void run() { System.out.println("Thread is running..."); } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } }
- 使用匿名内部类
public class Main { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { public void run() { System.out.println("Thread is running..."); } }); thread.start(); // 启动线程 } }
- 使用Lambda表达式
public class Main { public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println("Thread is running..."); }); thread.start(); // 启动线程 } }
- 实现Runnable接口
-
39.runnable和callable的区别?
1、方法不同
-
Runnable接口只有一个run()方法,该方法不返回任何值,因此无法抛出任何checked Exception。
-
Callable接口则有一个call()方法,它可以返回一个值,并且可以抛出一个checked Exception。
2、返回值不同
-
Runnable的run()方法没有返回值,只是一个void类型的方法。
-
Callable的call()方法却必须有一个返回值,并且返回值的类型可以通过泛型进行指定。
3、异常处理不同
-
在Runnable中,我们无法对run()方法抛出的异常进行任何处理。
-
在Callable中,自定义的call()方法可以抛出一个checked Exception,并由其执行者Handler进行捕获并处理。
4、使用场景不同
-
Runnable适用于那些不需要返回值,且不会抛出checked Exception的情况,比如简单的打印输出或者修改一些共享的变量。
-
Callable适用于那些需要返回值或者需要抛出checked Exception的情况,比如对某个任务的计算结果进行处理,或者需要进行网络或IO操作等。在Java中,常常使用Callable来实现异步任务的处理,以提高系统的吞吐量和响应速度。
-
-
40.线程有哪些状态?
线程有6种状态:New、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。且在同一时刻只能处于一种状态。
- NEW
当线程被创建出来还没有被调用start()时候的状态。
public class NewState { public static void main(String[] args) { Thread thread1 = new Thread("thread1"); System.out.println(thread1.getState()); } }
- RUNNABLE
当线程被调用了start(),且处于等待操作系统分配资源(如CPU)、等待IO连接、正在运行状态,即表示Running状态和Ready状态。
注意:不一定被调用了start()立刻会改变状态,还有一些准备工作,这个时候的状态是不确定的。public class RunnableState { public static void main(String[] args) { Thread thread1 = new Thread("thread1"); thread1.start(); System.out.println(thread1.getState()); } }
- BLOCKED
等待监视器锁而被阻塞的线程的线程状态,当进入synchronized块/方法或者在调用wait()被唤醒/超时之后重新进入synchronized块/方法,但是锁被其它线程占有,这个时候被操作系统挂起,状态为阻塞状态。
注意:阻塞状态的线程,即使调用interrupt()方法也不会改变其状态。import java.util.concurrent.TimeUnit; public class BlockedState { static String lock = "锁"; public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { synchronized (lock) { //死循环导致thread1一直持有lock对象锁 while (true) ; } } }; thread1.start(); //休眠1秒,让thread1先启动 TimeUnit.SECONDS.sleep(1); Thread thread2 = new Thread("thread2") { @Override public void run() { synchronized (lock) { //@1 System.out.println("thread2"); } } }; thread2.start(); System.out.println("thread1.state:" + thread1.getState()); System.out.println("thread2.state:" + thread2.getState()); } }
- WAITING
无条件等待,当线程调用wait()/join()/LockSupport.park()不加超时时间的方法之后所处的状态,如果没有被唤醒或等待的线程没有结束,那么将一直等待,当前状态的线程不会被分配CPU资源和持有锁。此时为无限等待。
导致线程处于WAITING的三种方式
方式1:waitimport java.util.concurrent.TimeUnit; public class WaitingState1 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { synchronized (WaitingState1.class) { try { //调用wait方法,让线程等待 WaitingState1.class.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread1.start(); //模拟休眠1秒,让thread1运行到wait方法处 TimeUnit.SECONDS.sleep(1); System.out.println("thread.state:" + thread1.getState()); } }
方式2:join()
public class WaitingState2 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { while (true) ; } }; thread1.start(); //join方法会让当前主线程等待thread1结束 thread1.join(); } }
方式3:LockSupport.park()
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; public class WaitingState3 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { LockSupport.park(); } }; thread1.start(); //模拟休眠1秒,让thread1运行到park方法处 TimeUnit.SECONDS.sleep(1); System.out.println("thread1.state:" + thread1.getState()); } }
-
- TIMED_WAITING
有条件的等待,当线程调用sleep(睡眠时间)/wait(等待时间)/join(等待时间)/ LockSupport.parkNanos(等待时间)/LockSupport.parkUntil(等待时间)方法之后所处的状态,在指定的时间没有被唤醒或者等待线程没有结束,会被系统自动唤醒,正常退出。
方式1import java.util.concurrent.TimeUnit; public class TimedWaitingState1 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { //休眠500秒 = 500000毫秒 try { Thread.sleep(500 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } }; thread1.start(); //模拟休眠1秒,让thread1运行到sleep方法处 TimeUnit.SECONDS.sleep(1); System.out.println("thread1.state:" + thread1.getState()); } }
-
- 方式2:wait(等待时间)
import java.util.concurrent.TimeUnit; public class TimedWaitingState2 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { synchronized (TimedWaitingState2.class) { try { //调用wait方法,让线程等待500秒 TimedWaitingState2.class.wait(500 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; thread1.start(); //模拟休眠1秒,让thread1运行到wait方法处 TimeUnit.SECONDS.sleep(1); System.out.println("thread1.state:" + thread1.getState()); } }
-
- 方式3:join(等待时间)
public class TimedWaitingState3 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { while (true) ; } }; thread1.start(); //join方法会让当前主线程等待thread1结束,最长等待500s,如果500s thread1.join(500 * 1000); } }
-
- 方式4: LockSupport.parkNanos(等待时间)
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; public class TimedWaitingState4 { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { //等待500秒 LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(500)); } }; thread1.start(); //模拟休眠1秒,让thread1运行到parkNanos方法处 TimeUnit.SECONDS.sleep(1); System.out.println("thread1.state:" + thread1.getState()); } }
- TERMINATED
执行完了run()方法。
import java.util.concurrent.TimeUnit; public class TerminatedState { public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread("thread1") { @Override public void run() { System.out.println(Thread.currentThread()); } }; thread1.start(); //休眠1秒,等待thread1执行完毕 TimeUnit.SECONDS.sleep(1); System.out.println("thread1 state:" + thread1.getState()); } }
-
41. sleep() 和 wait() 有什么区别?
所属类和方法签名
sleep-
Thread类。
-
方法签名:public static void sleep(long millis) throws InterruptedException。
-
参数:毫秒的睡眠时间。
-
异常:当前线程在等待睡眠期间被中断,则会抛出InterruptedException异常。
wait
-
Object类。
-
方法签名:
-
public final void wait() throws InterruptedException
-
public final void wait(long timeout) throws InterruptedException
-
public final void wait(long timeout, int nanos) throws InterruptedException
-
-
参数:
-
timeout:以毫秒为单位的等待时间。
-
nanos:额外的纳秒等待时间(0 到 999999 之间)。
-
-
异常:如果当前线程在等待期间被中断,则会抛出InterruptedException异常。
-
必须在同步块中使用。
释放锁
sleep()- 调用 sleep()`的线程不会释放它所持有的任何锁。
wait()
- 调用 wait()的线程会释放它所持有的对象锁,进入等待状态,直到被其他线程通过 notify()或 notifyAll()方法唤醒,或者等待时间结束。
使用场景
sleep()-
通常用于让当前线程休眠一段时间,不依赖于对象的锁。
-
常用于模拟延迟、暂停线程执行等场景。
wait()
-
通常用于线程间的通信,特别是在生产者-消费者模型中。
-
用于让线程等待某个条件成立,通常与 notify()和 notifyAll()配合使用。
唤醒机制
sleep()- 线程会在指定的睡眠时间结束后自动唤醒。
wait()
- 线程可以通过调用对象的 notify() 或 notifyAll()`方法被唤醒,或者等待时间结束后自动唤醒。
-
-
42. notify()和 notifyAll()有什么区别?
1、用途
-
notify():通常用于唤醒单个等待在该对象上的线程。
-
notifyAll():用于唤醒所有等待在该对象上的线程。
2、阻塞方式
-
notify():使用notify()唤醒单个线程时,其他线程不会被阻塞,可以继续执行。
-
notifyAll():唤醒所有线程时,所有正在等待该对象的线程都会被唤醒,然后它们会尝试重新进入同步代码块或方法。
3、线程安全
-
notify():唤醒单个线程时更加安全。
-
notifyAll():如果有多个线程同时尝试进入同步代码块或方法,可能会导致数据的不一致性。
4、性能
-
notify():可以更快地唤醒单个线程,因此可能在某些情况下提供更好的性能。
-
notifyALl():会导致更多的线程重新进入同步代码块或方法,这可能会消耗更多的CPU资源。
-
-
43. 线程的 run() 和 start() 有什么区别?
run()
- 这是线程要执行的任务代码所在的方法。当你重写Thread类或实现Runnable接口时,你需要提供一个run()方法的实现,该方法包含了线程执行的具体逻辑。直接调用run()方法就像调用一个普通的Java方法一样,它会在当前线程中顺序执行,而不会创建新线程。这意味着调用run()时,程序的执行仍然是单线程的,不会实现并发。
start()
- 当你创建了一个线程对象后,需要通过调用它的start()方法来启动这个线程。start()方法的作用是安排线程在Java虚拟机中开始执行,它会启动一个新线程,并由JVM负责调用该线程的run()方法。调用start()后,线程进入就绪状态,等待CPU分配时间片来实际执行run()方法中的代码。因此,start()不仅启动了一个新线程,还确保了线程的并发执行。
-
44. 创建线程池有哪几种方式?
线程池的创建有7种:
-
Executors.newFixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待;
-
Executors.newCachedThreadPool:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程;
-
Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执行顺序;
-
Executors.newScheduledThreadPool:创建一个可以执行延迟任务的线程池;
-
Executors.newSingleThreadScheduledExecutor:创建一个单线程的可以执行延迟任务的线程池;
-
Executors.newWorkStealingPool:创建一个抢占式执行的线程池(任务执行顺序不确定)【JDK 1.8 添加】。
-
ThreadPoolExecutor:最原始的创建线程池的方式,它包含了 7 个参数可供设置。
FixedthreadPoolF
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。public static void fixedThreadPool() { // 创建 2 个数据级的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 创建任务 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("任务被执行,线程:" + Thread.currentThread().getName()); } }; // 线程池执行任务(一次添加 4 个任务) // 执行任务的方法有两种:submit 和 execute threadPool.submit(runnable); // 执行方式 1:submit threadPool.execute(runnable); // 执行方式 2:execute threadPool.execute(runnable); threadPool.execute(runnable); }
简化如下
public static void fixedThreadPool() { // 创建线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 执行任务 threadPool.execute(() -> { System.out.println("任务被执行,线程:" + Thread.currentThread().getName()); }); }
CachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。public static void cachedThreadPool() { // 创建线程池 ExecutorService threadPool = Executors.newCachedThreadPool(); // 执行任务 for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println("任务被执行,线程:" + Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); } } 此代码共创建了10各个线程
SingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。public static void singleThreadExecutor() { // 创建线程池 ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + ":任务被执行"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); } }
ScheduledThreadPoll
创建一个可以执行延迟任务的线程池。public static void scheduledThreadPool() { // 创建线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5); // 添加定时执行任务(1s 后执行) System.out.println("添加任务,时间:" + new Date()); threadPool.schedule(() -> { System.out.println("任务被执行,时间:" + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 1, TimeUnit.SECONDS); } 任在1秒后执行务
SingleThreadScheduledExcutor
创建一个单线程的可以执行延迟任务的线程池。public static void SingleThreadScheduledExecutor() { // 创建线程池 ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor(); // 添加定时执行任务(2s 后执行) System.out.println("添加任务,时间:" + new Date()); threadPool.schedule(() -> { System.out.println("任务被执行,时间:" + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 2, TimeUnit.SECONDS); } 任务在2秒后执行
newWorkStealingPool
创建一个抢占式执行的线程池(任务执行顺序不确定),注意此方法只有在 JDK 1.8+ 版本中才能使用public static void workStealingPool() { // 创建线程池 ExecutorService threadPool = Executors.newWorkStealingPool(); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName()); }); } // 确保任务执行完成 while (!threadPool.isTerminated()) { } } 任务的执行顺序不确定,因为是抢占式执行的
ThreadPoolExecutor
最原始的线程池创建方法public static void myThreadPoolExecutor() { // 创建线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10)); // 执行任务 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); } }
-
-
45. 线程池都有哪些状态?
5种状态:Running、ShutDown、Stop、Tidying、Terminated。
Running-
此状态能够接收新任务,以及对已添加的任务进行处理。
-
线程池的初始化状态是RUNNING。线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0。
SHUTDOWN
-
线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
-
调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
STOP
-
线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
-
调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
TIDYING
-
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
-
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
TERMINATED
-
线程池彻底终止,就变成TERMINATED状态。
-
线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING -> TERMINATED。
-
-
46. 线程池中 submit() 和 execute() 方法有什么区别?
submit()
- 参数类型:submit() 方法有多个重载版本:
- submit(Runnable task):接受一个实现了 Runnable 接口。
- submit(Callable task):接受一个实现了 Callable接口的任务,Callable接口允许任务返回一个结果或抛出已检查的异常。
- 返回值:submit()方法返回一个
Future<?>
对象(对于Runnable
任务)或Future<T>
对象(对于 Callable任务)。这个 Future对象可以用来检查任务是否完成、等待任务完成、获取任务的结果(对于 Callable任务)或取消任务。 - 异常处理:对于
Callable
任务,如果任务执行过程中抛出了异常,这个异常会被封装在返回的Future
对象中,可以通过 Future.get() 方法抛出。 - 使用场景:适用于需要获取任务执行结果或需要处理任务执行过程中抛出的已检查异常的场景。
execute()
- 参数类型:
execute()
方法接受一个实现了Runnable
接口的任务。 - 返回值:
execute()
方法没有返回值(即返回void
)。 - 异常处理:如果任务在执行过程中抛出了未检查的异常(
RuntimeException
或Error
),这个异常会由线程池捕获并处理(通常是记录日志并终止线程)。对于已检查的异常(即非运行时异常),需要在Runnable
的run()
方法内部自行处理。 - 使用场景:适用于不需要获取任务执行结果或不需要处理任务执行过程中抛出的已检查异常的场景。
使用案例
execute()ExecutorService executor = Executors.newFixedThreadPool(2); executor.execute(new Runnable() { @Override public void run() { try { // 任务逻辑 System.out.println("Task executed using execute()"); } catch (Exception e) { // 处理异常 e.printStackTrace(); } } }); executor.shutdown();
submit()
ExecutorService executor = Executors.newFixedThreadPool(2); Future<?> future = executor.submit(new Runnable() { @Override public void run() { // 任务逻辑 System.out.println("Task executed using submit()"); } }); try { // 等待任务完成(可选) future.get(); } catch (InterruptedException | ExecutionException e) { // 处理异常 e.printStackTrace(); } executor.shutdown();
对于Callablea任务
ExecutorService executor = Executors.newFixedThreadPool(2); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() throws Exception { // 任务逻辑 return "Result from Callable task"; } }); try { // 获取任务结果 String result = future.get(); System.out.println(result); } catch (InterruptedException | ExecutionException e) { // 处理异常 e.printStackTrace(); (); } executor.shutdown();
- 参数类型:submit() 方法有多个重载版本: