🏆作者简介,普修罗双战士,一直追求不断学习和成长,在技术的道路上持续探索和实践。
🏆多年互联网行业从业经验,历任核心研发工程师,项目技术负责人。
🎉欢迎 👍点赞✍评论⭐收藏
🔎 并发编程专业知识 🔎
链接 | 专栏 |
---|---|
Java 并发编程专业知识学习一 | 并发编程专栏 |
Java 并发编程专业知识学习二 | 并发编程专栏 |
Java 并发编程专业知识学习三 | 并发编程专栏 |
Java 并发编程专业知识学习四 | 并发编程专栏 |
Java 并发编程专业知识学习五 | 并发编程专栏 |
Java 并发编程专业知识学习六 | 并发编程专栏 |
Java 并发编程专业知识学习七 | 并发编程专栏 |
Java 并发编程专业知识学习八 | 并发编程专栏 |
文章目录
- 并发编程面试题及答案(5)
- 01、 JAVA弱引用?
- 02、 什么是stackoverflow错误,permgen space错误?
- 03、 Java 中你怎样唤醒一个阻塞的线程?
- 04、 创建线程的四种方式?
- 05、 说一下 runnable 和 callable 有什么区别?
- 06、 阻塞队列和非阻塞队列区别?
- 07、 类加载有几个过程?
- 08、 线程 B 怎么知道线程 A 修改了变量?
- 09、 如何停止一个正在运行的线程?
- 10、 什么是线程死锁?
- 11、 常用的并发工具类有哪些?
- 12、 如何让正在运行的线程暂停一段时间?
- 13、 synchronized可重入的原理?
- 14、 并发队列的常用方法?
- 15、 线程的 sleep()方法和 yield()方法有什么区别?
- 16、 ThreadLocal是什么?有什么用?
- 17、 说一下 Atomic的原理?
- 18、 Java 中能创建 volatile 数组吗?
- 19、 Java中notify 和 notifyAll有什么区别?
- 20、 ReadWriteLock是什么?
- 21、 synchronized 和 Lock 有什么区别?
- 22、 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
- 23、 介绍newCachedThreadPool的使用?
- 24、 synchronized 和 ReentrantLock 区别是什么?
- 25、 在Java中Executor和Executors的区别?
- 26、 synchronized 和 volatile 的区别是什么?
- 27、 Java中的ReadWriteLock是什么?
- 28、 什么是重排序?
- 29、 为什么 Thread 类的 sleep()和 yield ()方法是静态的?
- 30、 请解释StackOverflowError和OutOfMemeryError的区别?
并发编程面试题及答案(5)
01、 JAVA弱引用?
Java中的弱引用(Weak Reference)是一种引用类型,用于指向对象但不会阻止对象被垃圾回收。当只有弱引用指向对象时,垃圾回收器在下一次垃圾回收时会回收该对象。
弱引用通常用于解决内存泄漏或缓存的问题。例如,当某个对象只有在被其他对象引用时才有意义,但又不希望该对象被强引用持有,可以使用弱引用来引用该对象。当没有其他强引用指向该对象时,垃圾回收器会回收该对象,并释放其占用的内存空间。
下面是一个使用弱引用的示例:
import java.lang.ref.WeakReference;
public class WeakReferenceExample {
public static void main(String[] args) {
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<>(obj);
obj = null; // 将强引用置为null
// 在此处可能进行垃圾回收,因为只有弱引用指向对象
System.gc();
// 弱引用可能已被垃圾回收,返回null
Object retrievedObj = weakRef.get();
if (retrievedObj == null) {
System.out.println("Object has been garbage collected.");
} else {
System.out.println("Object is still reachable.");
}
}
}
在上述示例中,创建了一个对象obj,并使用弱引用weakRef来引用该对象。将obj的强引用置为null后,垃圾回收器有可能在System.gc()调用时回收该对象。通过调用weakRef.get()方法,判断弱引用是否还能获取到对象。如果返回的retrievedObj为null,则表示对象已被垃圾回收,否则表示对象仍然可达。
需要注意的是,弱引用并不保证在垃圾回收时一定会被回收,垃圾回收的时机和行为由垃圾回收器决定。弱引用的主要作用是提供一种机制,使得对象能够在没有强引用时被垃圾回收。
02、 什么是stackoverflow错误,permgen space错误?
stackoverflow 错误是指程序在执行过程中发生了堆栈溢出的错误。当一个方法被递归调用或者方法调用层次过深时,堆栈中的帧会不断增加,当超出了堆栈的最大深度限制时,就会抛出 StackOverflowError 异常。
举个例子,假设有一个递归函数来计算阶乘,当输入的数值过大时,就容易发生stackoverflow错误。例如,以下是一个简单的递归计算阶乘的函数:
public static int factorial(int n) {
if (n == 0) {
return 1;
}
return n * factorial(n - 1);
}
public static void main(String[] args) {
int result = factorial(10000);
System.out.println(result);
}
在上述代码中,当调用 factorial(10000)
时,由于递归的层次太深,堆栈空间会被耗尽,导致发生 StackOverflowError 错误。
另外,PermGen Space 错误是指永久代空间溢出错误。在 Java 8 及之前的版本中,Java 虚拟机使用永久代来存储类的元数据(如类名、方法名等)和静态变量。当应用程序动态加载大量类或者频繁地进行类的加载和卸载时,永久代空间可能会被耗尽,导致发生 PermGen Space 错误。
举个例子,假设有一个应用程序使用动态代理来生成大量的类,并且这些类的加载和卸载非常频繁。在这种情况下,永久代空间可能会逐渐耗尽,最终导致 PermGen Space 错误的发生。
需要注意的是,从 Java 8 开始,永久代被元空间(Metaspace)取代,因此 PermGen Space 错误在 Java 8 及之后的版本中不再出现,取而代之的是 Metaspace 错误。
03、 Java 中你怎样唤醒一个阻塞的线程?
在 Java 中,我们可以使用 notify()
和 notifyAll()
方法来唤醒一个阻塞的线程。
举个例子,假设有一个生产者-消费者模型,其中生产者线程在队列满时会被阻塞,消费者线程在队列空时会被阻塞。我们可以使用 wait()
方法使线程进入阻塞状态,并使用 notify()
方法来唤醒等待的线程。
下面是一个简单的示例代码:
import java.util.Queue;
public class ProducerConsumerExample {
private Queue<Integer> queue;
public ProducerConsumerExample(Queue<Integer> queue) {
this.queue = queue;
}
public void produce() throws InterruptedException {
synchronized (queue) {
// 检查队列是否已满
while (queue.size() >= 10) {
System.out.println("队列已满,生产者线程进入等待状态");
queue.wait(); // 阻塞生产者线程
}
// 执行生产操作
int item = 1; // 假设要生产的数据为1
queue.add(item);
System.out.println("生产者生产了一个数据:" + item);
// 唤醒等待的消费者线程
queue.notifyAll();
}
}
public void consume() throws InterruptedException {
synchronized (queue) {
// 检查队列是否为空
while (queue.isEmpty()) {
System.out.println("队列为空,消费者线程进入等待状态");
queue.wait(); // 阻塞消费者线程
}
// 执行消费操作
int item = queue.poll();
System.out.println("消费者消费了一个数据:" + item);
// 唤醒等待的生产者线程
queue.notifyAll();
}
}
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
ProducerConsumerExample example = new ProducerConsumerExample(queue);
Thread producerThread = new Thread(() -> {
try {
while (true) {
example.produce();
Thread.sleep(1000); // 生产一个数据后休眠1秒
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
while (true) {
example.consume();
Thread.sleep(1000); // 消费一个数据后休眠1秒
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,生产者线程在队列满时调用 queue.wait()
进入等待状态,消费者线程在队列空时调用 queue.wait()
进入等待状态。当生产者生产一个数据后,调用 queue.notifyAll()
唤醒等待的消费者线程;当消费者消费一个数据后,调用 queue.notifyAll()
唤醒等待的生产者线程。这样,生产者和消费者之间就可以实现同步和通信。
04、 创建线程的四种方式?
创建线程的四种方式包括:
- 继承 Thread 类:创建一个类并继承 Thread 类,重写 run() 方法来定义线程的执行逻辑。然后通过创建该类的实例并调用 start() 方法来启动线程。
示例代码如下:
public class MyThread extends Thread {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("线程执行中");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}
- 实现 Runnable 接口:创建一个类实现 Runnable 接口,实现 run() 方法来定义线程的执行逻辑。然后通过创建 Thread 类的实例,将实现了 Runnable 接口的类作为参数传入,并调用 start() 方法来启动线程。
示例代码如下:
public class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("线程执行中");
}
public static void main(String[] args) {
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
- 使用 Callable 和 Future:创建一个类实现 Callable 接口,实现 call() 方法来定义线程的执行逻辑,并返回一个结果。然后通过创建 ExecutorService 的实例,调用 submit() 方法提交 Callable 对象,并通过 Future 对象获取线程的执行结果。
示例代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程执行的逻辑
return "线程执行中";
}
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(callable);
String result = future.get();
System.out.println(result);
executor.shutdown();
}
}
- 使用线程池:通过创建 ExecutorService 的实例,调用其提供的方法来提交任务并执行线程。线程池会自动管理线程的创建和销毁,提供了更好的线程资源利用和线程管理。
示例代码如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executor.execute(new Runnable() {
@Override
public void run() {
// 线程执行的逻辑
System.out.println("线程执行中");
}
});
}
executor.shutdown();
}
}
以上是四种常见的创建线程的方式,根据实际需求选择合适的方式来创建和管理线程。
05、 说一下 runnable 和 callable 有什么区别?
Runnable 和 Callable 都是用于创建线程的接口,但它们有以下几个区别:
-
返回值类型:Runnable 的 run() 方法没有返回值,而 Callable 的 call() 方法有返回值。
-
异常处理:Runnable 的 run() 方法不能抛出任何受检查异常,而 Callable 的 call() 方法可以抛出受检查异常。
-
使用方式:Runnable 接口通常用于创建没有返回值的简单线程任务,而 Callable 接口通常用于创建有返回值的复杂线程任务。
下面是一个示例代码,展示了如何使用 Runnable 和 Callable:
import java.util.concurrent.*;
public class RunnableVsCallable {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// 使用 Runnable 创建线程任务
Runnable runnableTask = () -> {
System.out.println("Runnable task is running");
};
Thread runnableThread = new Thread(runnableTask);
runnableThread.start();
// 使用 Callable 创建线程任务
Callable<String> callableTask = () -> {
System.out.println("Callable task is running");
return "Callable task result";
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(callableTask);
String result = future.get();
System.out.println("Callable task result: " + result);
executor.shutdown();
}
}
在上述代码中,我们首先通过实现 Runnable 接口创建了一个简单的线程任务,并通过 Thread 类将其包装为线程来执行。然后,我们使用 Callable 接口创建了一个复杂的线程任务,并通过 ExecutorService 提交任务并获得 Future 对象来获取任务的返回值。最后,我们使用 ExecutorService 关闭线程池。
请注意,Callable 接口的 call() 方法可以通过 Future 对象的 get() 方法获取到返回值,而 Runnable 接口的 run() 方法没有返回值。
06、 阻塞队列和非阻塞队列区别?
阻塞队列和非阻塞队列是在多线程编程中常用的数据结构,它们的主要区别在于在队列为空或已满时的处理方式。
- 阻塞队列(Blocking Queue):当队列为空时,从队列中获取元素的操作会被阻塞,直到队列中有可用元素;当队列已满时,向队列中添加元素的操作会被阻塞,直到队列有空闲位置。阻塞队列能够自动处理线程的等待和唤醒,使得线程在队列为空或已满时能够正确地进行阻塞和等待。
常见的阻塞队列实现类有:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue等。
示例代码:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<>(5);
// 生产者线程
Thread producerThread = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
blockingQueue.put(i);
System.out.println("生产者生产:" + i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
try {
for (int i = 1; i <= 10; i++) {
int num = blockingQueue.take();
System.out.println("消费者消费:" + num);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
producerThread.join();
consumerThread.join();
}
}
在上述代码中,我们使用 ArrayBlockingQueue 作为阻塞队列的实现类。生产者线程会向队列中添加元素,当队列已满时会被阻塞,直到队列有空闲位置。消费者线程会从队列中取出元素,当队列为空时会被阻塞,直到队列中有可用元素。
- 非阻塞队列(Non-blocking Queue):当队列为空时,从队列中获取元素的操作会立即返回 null 或抛出异常;当队列已满时,向队列中添加元素的操作会立即返回 false 或抛出异常。非阻塞队列不会进行线程的等待和唤醒,需要手动处理线程的等待和唤醒。
常见的非阻塞队列实现类有:ConcurrentLinkedQueue、LinkedTransferQueue、SynchronousQueue等。
示例代码:
import java.util.concurrent.ConcurrentLinkedQueue;
public class NonBlockingQueueExample {
public static void main(String[] args) {
ConcurrentLinkedQueue<Integer> nonBlockingQueue = new ConcurrentLinkedQueue<>();
// 生产者线程
Thread producerThread = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
nonBlockingQueue.offer(i);
System.out.println("生产者生产:" + i);
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
for (int i = 1; i <= 10; i++) {
Integer num = nonBlockingQueue.poll();
if (num != null) {
System.out.println("消费者消费:" + num);
}
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,我们使用 ConcurrentLinkedQueue 作为非阻塞队列的实现类。生产者线程会向队列中添加元素,当队列已满时会立即返回 false。消费者线程会从队列中取出元素,当队列为空时会立即返回 null。
总结:阻塞队列和非阻塞队列的区别在于在队列为空或已满时的处理方式。阻塞队列会自动处理线程的等待和唤醒,而非阻塞队列需要手动处理线程的等待和唤醒。根据实际需求和场景选择合适的队列实现。
07、 类加载有几个过程?
类加载过程主要分为加载(Loading)、链接(Linking)和初始化(Initialization)三个阶段。
-
加载(Loading):加载阶段是将类的字节码文件加载到内存中的过程。类加载器根据类的全限定名找到对应的字节码文件,并将其读取到内存中。在加载阶段,还会进行验证字节码文件的正确性,确保其符合 JVM 规范。
-
链接(Linking):链接阶段主要包括验证(Verification)、准备(Preparation)和解析(Resolution)三个步骤。
- 验证(Verification):验证阶段会对字节码文件进行校验,确保其符合 JVM 规范和安全要求。
- 准备(Preparation):准备阶段会为类的静态变量分配内存,并设置默认初始值。
- 解析(Resolution):解析阶段会将类、接口、字段和方法的符号引用转化为直接引用。
-
初始化(Initialization):初始化阶段是类加载过程的最后一步,也是执行类构造器(即
<clinit>
方法)的阶段。在初始化阶段,会执行类的静态变量赋值和静态代码块中的代码,并且按照顺序执行。初始化阶段标志着类的准备工作完成,可以正常使用该类。
示例代码如下:
public class MyClass {
public static final int NUMBER = 10; // 静态变量
static {
System.out.println("静态代码块执行");
}
public static void main(String[] args) {
System.out.println("主方法执行");
}
}
在上述代码中,当 JVM 加载 MyClass 类时,会先进行加载阶段,将 MyClass 的字节码文件加载到内存中。然后,在链接阶段,会进行验证、准备和解析。最后,在初始化阶段,会执行静态变量的赋值和静态代码块中的代码。当执行
main
方法时,会输出 “静态代码块执行” 和 “主方法执行”。这表明类的加载、链接和初始化阶段已经顺利完成。
08、 线程 B 怎么知道线程 A 修改了变量?
线程 B 可以通过使用同步机制来知道线程 A 是否修改了变量。同步机制包括使用锁(synchronized)或其他线程间通信的方式。
举个例子,假设有两个线程 A 和 B,它们共享一个变量 count
。线程 A 负责递增 count
的值,线程 B 需要在线程 A 修改 count
后得到变量的新值。
使用锁的方式,可以通过在共享代码块或方法中使用 synchronized 关键字来实现同步。示例代码如下:
public class ThreadCommunicationExample {
private static int count = 0;
public static synchronized void increment() {
count++;
System.out.println("线程 A 修改了 count");
}
public static void main(String[] args) throws InterruptedException {
Thread threadA = new Thread(() -> {
increment();
});
Thread threadB = new Thread(() -> {
synchronized (ThreadCommunicationExample.class) {
try {
ThreadCommunicationExample.class.wait(); // 线程 B 等待
System.out.println("线程 B 获取到了 count 的新值:" + count);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
threadB.start();
Thread.sleep(1000); // 等待一段时间,确保线程 B 在线程 A 修改 count 后开始执行
threadA.start();
threadA.join();
threadB.join();
}
}
在上述代码中,线程 A 使用 synchronized 修饰的 increment()
方法修改 count
的值,并在修改后调用 notifyAll()
方法通知等待的线程。线程 B 在获取 count
的新值之前通过 wait()
方法进入等待状态,直到被线程 A 唤醒。当线程 B 被唤醒后,它可以获取到线程 A 修改后的 count
的新值。
通过这种方式,线程 B 可以在线程 A 修改变量后得到变量的新值,并进行相应的处理。需要注意的是,同步机制的使用需要谨慎,确保线程间的通信和同步操作正确无误。
09、 如何停止一个正在运行的线程?
停止一个正在运行的线程可以通过设置一个标志位或者使用 Thread 类提供的 interrupt() 方法来实现。具体的实现方式取决于线程的执行逻辑和需求。
举个例子,假设有一个线程执行一个循环操作,我们希望在外部停止该线程。可以通过设置一个标志位来控制线程的执行状态。示例代码如下:
public class StoppableThread extends Thread {
private volatile boolean isRunning = true;
@Override
public void run() {
while (isRunning) {
// 执行线程的逻辑
System.out.println("线程执行中");
}
System.out.println("线程停止");
}
public void stopThread() {
isRunning = false;
}
public static void main(String[] args) throws InterruptedException {
StoppableThread thread = new StoppableThread();
thread.start();
Thread.sleep(2000); // 等待2秒钟
thread.stopThread(); // 停止线程
thread.join();
}
}
上述代码中,我们创建了一个 StoppableThread 类,其中包含一个标志位 isRunning 来控制线程的执行状态。在 run() 方法中,线程会在循环中执行逻辑,只要 isRunning 为 true,线程就会继续执行。通过调用 stopThread() 方法,我们可以将 isRunning 设置为 false,从而停止线程的执行。
需要注意的是,线程的停止应该是协作式的,即通过设置标志位或调用 interrupt() 方法来通知线程停止,而不是强制终止线程。这样可以确保线程在停止前完成必要的清理工作,并避免可能的资源泄漏或数据不一致等问题。
10、 什么是线程死锁?
线程死锁是指两个或多个线程互相持有对方所需的资源,并且无法继续执行,导致程序无法继续运行的情况。
举个例子,假设有两个线程 A 和 B,它们分别持有资源 X 和资源 Y,并且它们都需要获取对方持有的资源才能继续执行。如果线程 A 先获取了资源 X,然后尝试获取资源 Y,同时线程 B 先获取了资源 Y,然后尝试获取资源 X,这时就会出现死锁的情况。
示例代码如下:
public class DeadlockExample {
private static final Object resourceX = new Object();
private static final Object resourceY = new Object();
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
synchronized (resourceX) {
System.out.println("Thread A acquired resource X");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceY) {
System.out.println("Thread A acquired resource Y");
}
}
});
Thread threadB = new Thread(() -> {
synchronized (resourceY) {
System.out.println("Thread B acquired resource Y");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resourceX) {
System.out.println("Thread B acquired resource X");
}
}
});
threadA.start();
threadB.start();
}
}
在上述代码中,线程 A 先获取了资源 X,然后尝试获取资源 Y;线程 B 先获取了资源 Y,然后尝试获取资源 X。由于两个线程互相持有对方所需的资源,并且无法释放,它们都无法继续执行,导致程序陷入死锁状态。
为了避免死锁,可以采取一些预防措施,如避免线程之间循环依赖资源、按照固定的顺序获取资源、设置超时时间等。死锁的处理是一个复杂的问题,需要仔细分析和设计,以确保线程安全和程序的正常运行。
11、 常用的并发工具类有哪些?
常用的并发工具类有以下几个:
-
CountDownLatch(倒计时门闩):用于控制线程等待,它允许一个或多个线程等待其他线程完成操作后再继续执行。
-
CyclicBarrier(循环屏障):用于多个线程之间相互等待,直到所有线程都到达一个同步点后再继续执行。
-
Semaphore(信号量):用于控制同时访问某个资源的线程数量,通过 acquire() 方法获取许可,release() 方法释放许可。
-
Exchanger(交换器):用于两个线程之间交换数据,它提供一个同步点,当两个线程都到达同步点时,可以交换数据。
-
Phaser(分阶段器):用于多个线程分阶段地执行任务,每个阶段的线程都必须等待其他线程完成后才能继续执行。
-
Lock(锁):用于实现更灵活的线程同步和互斥,常见的实现类有 ReentrantLock 和 ReentrantReadWriteLock。
-
Condition(条件):与 Lock 配合使用,可以实现更复杂的线程等待和唤醒机制。
这些并发工具类提供了方便的方法来处理并发编程中的线程同步、线程等待等问题,可以根据具体的需求选择合适的工具类来使用。
- CountDownLatch:用于控制线程等待,它允许一个或多个线程等待其他线程完成操作后再继续执行。
示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 线程执行的逻辑
System.out.println("线程执行中");
latch.countDown();
});
thread.start();
}
latch.await(); // 等待所有线程执行完成
System.out.println("所有线程执行完成");
}
}
- CyclicBarrier:用于多个线程之间相互等待,直到所有线程都到达一个同步点后再继续执行。
示例代码:
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierExample {
public static void main(String[] args) {
int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount, () -> {
// 所有线程到达同步点后执行的逻辑
System.out.println("所有线程已到达同步点");
});
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
// 线程执行的逻辑
System.out.println("线程执行中");
try {
barrier.await(); // 等待其他线程到达同步点
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
- Semaphore:用于控制同时访问某个资源的线程数量,通过 acquire() 方法获取许可,release() 方法释放许可。
示例代码:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int threadCount = 5;
Semaphore semaphore = new Semaphore(3); // 最多允许3个线程同时访问
for (int i = 0; i < threadCount; i++) {
Thread thread = new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
// 线程执行的逻辑
System.out.println("线程执行中");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可
}
});
thread.start();
}
}
}
以上是常用的几个并发工具类,它们提供了方便的方法来处理并发编程中的线程同步、线程等待等问题。根据具体的需求,选择合适的并发工具类可以提高代码的可读性和可维护性。
12、 如何让正在运行的线程暂停一段时间?
可以使用Thread类的 sleep()
方法来让正在运行的线程暂停一段时间。 sleep()
方法接受一个以毫秒为单位的时间参数,指定线程暂停的时间长度。
以下是一个示例代码:
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
System.out.println("线程开始执行");
Thread.sleep(2000); // 线程暂停2秒钟
System.out.println("线程继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
在上述代码中,我们创建了一个线程,线程在执行过程中调用 Thread.sleep(2000)
来暂停2秒钟。当线程执行到 sleep()
方法时,它会暂停执行2秒钟,然后继续执行后面的代码。
需要注意的是, sleep()
方法会抛出 InterruptedException
异常,因此需要进行异常处理。此外, sleep()
方法会让当前线程进入阻塞状态,但不会释放持有的锁。
13、 synchronized可重入的原理?
synchronized 是可重入的,也就是说一个线程可以多次获得同一个锁而不会产生死锁。这种机制是通过线程持有的锁对象上维护一个计数器来实现的。
当一个线程第一次获得锁时,计数器的值为1。在此期间,该线程可以多次进入同步代码块,并且每次进入都会增加计数器的值。当线程退出同步代码块时,计数器的值会递减。只有当计数器的值为0时,锁才会被完全释放,其他线程才能获得该锁。
举个例子,假设有一个类 A,其中包含两个同步方法 methodA()
和 methodB()
。当一个线程调用 methodA()
方法时,它会获得 A 对象上的锁,并且计数器的值为1。在 methodA()
方法中,它又调用了 methodB()
方法,此时由于是在同一个线程中,线程可以再次获得 A 对象上的锁,计数器的值增加为2。当线程退出 methodB()
方法后,计数器的值减少为1。最后,当线程退出 methodA()
方法时,计数器的值减少为0,锁被完全释放。
示例代码如下:
public class ReentrantExample {
public synchronized void methodA() {
System.out.println("进入 methodA");
methodB();
System.out.println("退出 methodA");
}
public synchronized void methodB() {
System.out.println("进入 methodB");
// methodB 的逻辑
System.out.println("退出 methodB");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.methodA();
}
}
在上述代码中,当调用 example.methodA()
方法时,线程会首先获得 example
对象上的锁,并且计数器的值为1。然后,在 methodA()
方法中,又调用了 methodB()
方法,由于是在同一个线程中,线程可以再次获得 example
对象上的锁,计数器的值增加为2。最后,线程退出 methodA()
方法后,计数器的值减少为1,退出 methodB()
方法后,计数器的值减少为0,锁被完全释放。
通过 synchronized 的可重入特性,线程可以安全地进入和退出同步代码块,避免了死锁和其他并发问题。
14、 并发队列的常用方法?
并发队列是一种支持多线程并发操作的数据结构,常用于多线程环境下的任务调度、消息传递等场景。常见的并发队列实现包括ConcurrentLinkedQueue、ArrayBlockingQueue、LinkedBlockingQueue等。
以下是并发队列的一些常用方法及其示例:
- add(element):向队列尾部添加一个元素,如果队列已满则抛出异常。
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
queue.add(1);
queue.add(2);
- offer(element):向队列尾部添加一个元素,如果队列已满则返回false。
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(3);
queue.offer("A");
queue.offer("B");
- put(element):向队列尾部添加一个元素,如果队列已满则阻塞等待。
LinkedBlockingQueue<Integer> queue = new LinkedBlockingQueue<>(2);
queue.put(1);
queue.put(2);
- remove():移除并返回队列头部的元素,如果队列为空则抛出异常。
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.add("A");
queue.add("B");
String element = queue.remove();
- poll():移除并返回队列头部的元素,如果队列为空则返回null。
ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<>(3);
queue.add(1);
queue.add(2);
Integer element = queue.poll();
- take():移除并返回队列头部的元素,如果队列为空则阻塞等待。
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("A");
queue.put("B");
String element = queue.take();
这些方法只是并发队列中的一部分常用方法,还有其他方法如size()、isEmpty()、peek()等。具体使用时,需要根据具体的并发场景和需求选择适合的并发队列实现及方法。
15、 线程的 sleep()方法和 yield()方法有什么区别?
线程的sleep()方法和yield()方法都是用于线程的调度和控制,但它们有不同的作用和效果。
- sleep()方法:
- sleep()方法使当前线程暂停执行一段时间,指定的时间段由参数指定,单位是毫秒。
- sleep()方法会让出CPU的执行时间,但不会释放锁资源。
- 调用sleep()方法的线程会进入阻塞状态,直到指定的时间过去或被其他线程中断。
- sleep()方法通常用于模拟耗时的操作或控制线程的执行速度。
举例:
public class SleepExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread A: " + i);
try {
Thread.sleep(1000); // 暂停1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
上述示例中,线程A每隔1秒输出一个数字,通过调用sleep()方法使线程暂停1秒。
- yield()方法:
- yield()方法使当前线程主动放弃CPU执行时间,让出给同等或更高优先级的线程。
- yield()方法只是给线程调度器一个提示,表明当前线程愿意让出执行时间,但不保证一定会让出。
- 调用yield()方法的线程会进入就绪状态,等待线程调度器重新分配CPU时间片。
举例:
public class YieldExample {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread A: " + i);
Thread.yield(); // 主动让出CPU执行时间
}
});
Thread threadB = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread B: " + i);
Thread.yield(); // 主动让出CPU执行时间
}
});
threadA.start();
threadB.start();
}
}
上述示例中,线程A和线程B交替输出数字,通过调用yield()方法主动让出CPU执行时间,增加了线程切换的机会。
总结:sleep()方法是让线程暂停执行一段时间,yield()方法是主动让出CPU执行时间。sleep()方法会进入阻塞状态,yield()方法会进入就绪状态。
16、 ThreadLocal是什么?有什么用?
ThreadLocal是Java中的一个类,用于在多线程环境下存储线程局部变量。它允许每个线程都拥有自己独立的变量副本,互不干扰。
ThreadLocal的主要用途是在多线程环境下共享数据时,保证数据的线程安全性。通过将数据存储在ThreadLocal对象中,每个线程都可以独立地访问和修改自己的数据副本,而不会影响其他线程的数据。
举个例子,假设有一个线程池,多个线程共享一个全局的数据库连接。如果直接在多个线程中共享同一个连接对象,可能会出现并发访问的问题。但是,如果使用ThreadLocal来存储每个线程的连接对象,每个线程都拥有自己独立的连接副本,就可以避免并发访问问题。
以下是一个简单的示例代码:
public class ConnectionManager {
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<>();
public static Connection getConnection() {
Connection connection = connectionHolder.get();
if (connection == null) {
connection = createConnection();
connectionHolder.set(connection);
}
return connection;
}
private static Connection createConnection() {
// 创建数据库连接的逻辑
}
}
在上面的代码中,每个线程通过 getConnection()
方法获取自己的数据库连接。如果线程第一次调用该方法时,会创建一个新的连接对象并存储到ThreadLocal中。以后每次调用 getConnection()
方法时,都会返回该线程自己的连接对象。
这样,每个线程都拥有独立的连接对象,可以并发地访问数据库,而不会相互干扰。
17、 说一下 Atomic的原理?
Atomic是Java中的一个原子类,用于实现线程安全的原子操作。它通过使用底层的CAS(Compare and Swap)机制来保证操作的原子性。
CAS是一种乐观锁的实现方式,它包含三个参数:内存地址V、旧的预期值A和新的值B。CAS操作会先比较内存地址V中的值与预期值A是否相等,如果相等,则将新的值B写入内存地址V;如果不相等,则说明内存地址V的值已经被其他线程修改,CAS操作失败,需要重新尝试。
Atomic类利用CAS机制来实现原子操作。当多个线程同时对Atomic对象进行修改时,CAS机制会确保只有一个线程能够成功修改该对象的值,其他线程需要重新尝试。
Atomic类提供了一些常见的原子操作方法,如getAndAdd、getAndSet、compareAndSet等。这些方法在执行时会利用CAS机制来保证操作的原子性。
举个例子,假设有多个线程同时对一个计数器进行递增操作。如果使用普通的变量进行操作,可能会出现并发访问问题。但是,如果使用AtomicInteger类来实现计数器,就可以保证递增操作的原子性。
以下是一个简单的示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
在上面的代码中,使用AtomicInteger类来实现计数器的递增操作。多个线程可以同时调用 increment()
方法来递增计数器的值,而不会出现并发访问问题。
Atomic类的原理就是利用CAS机制来确保操作的原子性,从而实现线程安全的原子操作。
18、 Java 中能创建 volatile 数组吗?
在Java中,不能直接创建volatile数组。volatile关键字只能应用于类的成员变量或静态变量,而不能应用于局部变量或数组。
然而,可以通过创建一个volatile数组的包装类来实现类似的效果。例如,可以创建一个包含volatile数组成员变量的类,并对该数组进行操作。通过在操作数组元素时保持对该数组的引用是volatile的,可以确保对数组的操作是可见的。
以下是一个示例代码:
public class VolatileArrayWrapper {
private volatile int[] array;
public VolatileArrayWrapper(int size) {
array = new int[size];
}
public int get(int index) {
return array[index];
}
public void set(int index, int value) {
array[index] = value;
}
}
在上面的代码中,通过创建一个包装类VolatileArrayWrapper,将需要操作的数组作为该类的成员变量,并使用volatile关键字修饰该数组。通过调用VolatileArrayWrapper类的方法来对数组进行操作,可以确保对数组的操作是可见的。
需要注意的是,volatile关键字只能保证对数组引用的可见性,而不能保证对数组元素的原子性操作。如果需要进行原子性操作,可以考虑使用其他的同步机制,如锁或Atomic类。
19、 Java中notify 和 notifyAll有什么区别?
Java中的notify和notifyAll是用于线程间通信的方法,用于唤醒等待中的线程。它们的区别如下:
-
notify:notify方法用于唤醒在对象的等待队列中等待的单个线程。如果有多个线程在等待,只会唤醒其中一个线程,具体唤醒哪个线程是不确定的,由JVM决定。
-
notifyAll:notifyAll方法用于唤醒在对象的等待队列中等待的所有线程。它会唤醒所有等待的线程,使它们重新竞争对象的锁。
举个例子,假设有一个生产者-消费者模型的场景,多个消费者线程等待生产者线程生产数据。当生产者线程生产了新的数据后,可以使用notify或notifyAll来唤醒等待的消费者线程。
以下是一个使用notify的简单示例代码:
public class ProducerConsumer {
private Object lock = new Object();
private boolean hasData = false;
public void produce() {
synchronized (lock) {
// 生产数据的逻辑
hasData = true;
lock.notify(); // 唤醒等待的线程
}
}
public void consume() {
synchronized (lock) {
while (!hasData) {
try {
lock.wait(); // 等待数据的到来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费数据的逻辑
hasData = false;
}
}
}
在上面的代码中,生产者线程在生产数据后,通过调用lock对象的notify方法来唤醒等待的消费者线程。消费者线程在没有数据可消费时,通过调用lock对象的wait方法来等待数据的到来。
如果将上述代码中的lock.notify()改为lock.notifyAll(),则会唤醒所有等待的消费者线程。
需要注意的是,notify和notifyAll方法必须在同步代码块中调用,并且是针对持有同一个对象锁的线程进行唤醒。
20、 ReadWriteLock是什么?
ReadWriteLock是Java中的一个接口,用于实现读写锁的机制。读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作,从而提高了读操作的并发性能。
下面是一个使用ReadWriteLock的简单示例代码:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedResource {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value;
public int getValue() {
lock.readLock().lock();
try {
return value;
} finally {
lock.readLock().unlock();
}
}
public void setValue(int newValue) {
lock.writeLock().lock();
try {
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
}
在上面的代码中,SharedResource类维护一个共享资源value,并使用ReadWriteLock来保护对该资源的读写操作。在读取value时,使用读锁(readLock)进行加锁,允许多个线程同时读取。在写入value时,使用写锁(writeLock)进行加锁,只允许一个线程进行写操作。
通过使用ReadWriteLock,多个线程可以同时读取value,从而提高了读操作的并发性能。而在写入value时,会独占锁,确保只有一个线程进行写操作,保证数据的一致性和正确性。
需要注意的是,使用ReadWriteLock时需要确保在读取和写入操作完成后正确释放锁,以避免死锁或其他线程同步问题。
21、 synchronized 和 Lock 有什么区别?
synchronized和Lock都是Java中用于实现线程同步的机制,它们的区别如下:
1. 可重入性
:synchronized是Java内置的关键字,具有隐式的获取和释放锁的机制,并且支持可重入性。可重入性指的是同一个线程在持有锁的情况下,可以再次获取该锁,而不会发生死锁。而Lock接口的实现类(如ReentrantLock)也支持可重入性,但需要显式地调用lock()和unlock()方法来获取和释放锁。
2. 锁的获取方式
:synchronized是隐式获取锁的方式,当线程进入synchronized代码块或方法时,会自动获取锁;当线程退出synchronized代码块或方法时,会自动释放锁。而Lock接口提供了更灵活的锁获取方式,可以通过lock()方法来获取锁,并通过unlock()方法来释放锁,可以更加细粒度地控制锁的获取和释放。
3. 锁的可中断性
:synchronized在获取锁时,如果其他线程已经持有锁,当前线程会进入阻塞状态,直到锁被释放。而Lock接口提供了可中断的获取锁的方式,即在等待锁的过程中,可以通过interrupt()方法中断等待的线程。
4. 条件变量
:Lock接口提供了Condition接口,可以通过Condition对象实现线程间的通信和协作。Condition对象可以与Lock对象绑定,通过await()方法使线程等待,通过signal()方法唤醒等待的线程。
下面是一个使用Lock的示例代码:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SharedResource {
private final Lock lock = new ReentrantLock();
private int value;
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
public void setValue(int newValue) {
lock.lock();
try {
value = newValue;
} finally {
lock.unlock();
}
}
}
在上面的代码中,SharedResource类维护一个共享资源value,并使用ReentrantLock来保护对该资源的读写操作。在读取value时,使用lock()方法获取锁,然后在try-finally块中返回value,并在finally块中释放锁。在写入value时,同样使用lock()方法获取锁,然后在try-finally块中更新value,并在finally块中释放锁。
通过使用Lock接口,我们可以更加细粒度地控制锁的获取和释放,实现更灵活的线程同步。需要注意的是,在使用Lock时,必须确保在获取锁后正确释放锁,以避免死锁或其他线程同步问题。
22、 为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里?
线程通信的方法wait()、notify()和notifyAll()被定义在Object类中的主要原因是,线程通信是基于对象的等待和通知机制实现的。
1. wait()方法
:wait()方法用于使当前线程进入等待状态,并释放对象的锁。它必须在synchronized代码块或方法中调用,并且会暂时释放锁,使其他线程可以进入临界区。当其他线程调用notify()或notifyAll()方法时,等待的线程会被唤醒,继续执行。
2. notify()方法
:notify()方法用于唤醒在对象上等待的单个线程。它会选择一个等待的线程进行唤醒,具体唤醒哪个线程是不确定的,由JVM决定。被唤醒的线程会进入就绪状态,等待获取对象的锁。
3. notifyAll()方法
:notifyAll()方法用于唤醒在对象上等待的所有线程。它会唤醒所有等待的线程,使它们进入就绪状态,等待获取对象的锁。
这些线程通信的方法被定义在Object类中的原因是,每个Java对象都有一个锁(monitor),并且每个对象都可以作为等待和通知的条件。因此,这些方法被定义在所有Java对象的共同父类Object中,以支持所有对象的通用线程通信机制。
以下是一个简单的示例代码,演示了线程通信的使用:
public class ThreadCommunicationExample {
private final Object lock = new Object();
private boolean isDataReady = false;
public void produceData() {
synchronized (lock) {
// 生产数据的逻辑
isDataReady = true;
lock.notify(); // 唤醒等待的线程
}
}
public void consumeData() {
synchronized (lock) {
while (!isDataReady) {
try {
lock.wait(); // 等待数据的到来
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费数据的逻辑
isDataReady = false;
}
}
}
在上面的代码中,produceData()方法用于生产数据,设置isDataReady为true,并通过lock.notify()方法唤醒等待的线程。consumeData()方法用于消费数据,当isDataReady为false时,通过lock.wait()方法等待数据的到来。当生产者生产数据后,会唤醒等待的消费者线程,消费者线程被唤醒后继续执行消费数据的逻辑。
23、 介绍newCachedThreadPool的使用?
newCachedThreadPool是Java中的一个线程池类,它是Executor框架中的一部分。它的主要特点是根据需要动态创建线程,并根据线程的可用性自动回收线程。
使用newCachedThreadPool非常简单。您只需要使用Executors类的静态方法之一来创建一个newCachedThreadPool实例。下面是一个示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建一个newCachedThreadPool实例
ExecutorService executor = Executors.newCachedThreadPool();
// 提交任务给线程池
executor.submit(new SomeTask());
executor.submit(new AnotherTask());
// 关闭线程池
executor.shutdown();
}
}
class SomeTask implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("SomeTask is running");
}
}
class AnotherTask implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("AnotherTask is running");
}
}
在上面的示例中,我们首先使用 Executors.newCachedThreadPool()
创建了一个newCachedThreadPool实例。然后,我们向线程池提交了两个任务:SomeTask和AnotherTask。这两个任务将在可用的线程上执行。最后,我们使用 executor.shutdown()
关闭线程池。
newCachedThreadPool会根据需要创建新的线程,如果线程池中有空闲线程可用,任务将会在这些空闲线程上执行。如果没有可用的空闲线程,它将创建一个新的线程来执行任务。当线程在60秒内没有被使用时,它们将被自动回收。
这种线程池适用于执行大量短期任务的场景,因为它可以动态地调整线程池的大小以适应任务的数量。
24、 synchronized 和 ReentrantLock 区别是什么?
synchronized和ReentrantLock都是Java中用于实现线程同步的机制,它们的作用是确保多个线程在访问共享资源时的互斥性。它们之间的区别如下:
1. 锁的获取方式
:synchronized是隐式锁,即在方法或代码块上使用关键字synchronized来获取锁,而ReentrantLock是显式锁,需要通过lock()方法来获取锁,并通过unlock()方法来释放锁。
2. 可重入性
:ReentrantLock是可重入锁,即同一个线程可以多次获取同一个锁,而synchronized是不可重入锁,同一个线程在获取锁之后再次尝试获取锁会导致死锁。
3. 锁的粒度
:synchronized是对整个方法或代码块加锁,锁的粒度较大,而ReentrantLock可以通过lock()和unlock()方法实现对代码中特定部分的加锁,锁的粒度较小,可以更加灵活地控制锁的范围。
4. 性能
:在低竞争情况下,synchronized的性能通常比ReentrantLock好,因为synchronized是由JVM内部实现的,而ReentrantLock是通过Java代码实现的。但在高竞争情况下,ReentrantLock的性能可能更好,因为它提供了更多的高级特性,如公平锁、可中断锁等。
下面举例说明它们的作用:
使用synchronized实现线程同步:
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
在上面的代码中,我们使用synchronized关键字来实现对Counter类中的方法的同步,确保多个线程对count变量的访问是互斥的。
使用ReentrantLock实现线程同步:
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count;
private ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
count--;
} finally {
lock.unlock();
}
}
public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
在上面的代码中,我们使用ReentrantLock来实现对Counter类中的方法的同步,同样确保多个线程对count变量的访问是互斥的。注意,在使用ReentrantLock时,需要在finally块中调用unlock()方法来释放锁。
25、 在Java中Executor和Executors的区别?
在Java中,Executor和Executors都是用于实现线程池的工具类。它们的区别如下:
-
Executor是一个接口,定义了线程池的基本行为,如提交任务、执行任务等。它提供了一种统一的方式来执行异步任务,隐藏了线程的创建和管理细节。Executor接口的常见实现类有ThreadPoolExecutor。
-
Executors是一个工具类,提供了一些静态方法来创建不同类型的线程池。它是Executor接口的一个实现,通过封装一些常用的线程池创建方式,简化了线程池的使用。Executors类的静态方法可以创建不同类型的线程池,如newFixedThreadPool、newCachedThreadPool等。
下面是一个示例,说明Executor和Executors的使用:
使用Executor创建线程池:
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池
Executor executor = Executors.newFixedThreadPool(3);
// 提交任务给线程池
executor.execute(new SomeTask());
executor.execute(new AnotherTask());
executor.execute(new ThirdTask());
}
}
class SomeTask implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("SomeTask is running");
}
}
class AnotherTask implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("AnotherTask is running");
}
}
class ThirdTask implements Runnable {
@Override
public void run() {
// 执行任务的逻辑
System.out.println("ThirdTask is running");
}
}
使用Executors创建线程池:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务给线程池
executor.submit(new SomeTask());
executor.submit(new AnotherTask());
executor.submit(new ThirdTask());
// 关闭线程池
executor.shutdown();
}
}
// 省略任务类的定义,与上面的示例相同
无论是使用Executor还是Executors,都可以方便地创建线程池并提交任务,实现线程的复用和管理。Executors类提供了一些常用的线程池创建方式,对于一些简单的场景,可以更加方便地创建线程池。而对于更复杂的线程池需求,可以使用Executor接口的实现类ThreadPoolExecutor来自定义线程池的行为。
26、 synchronized 和 volatile 的区别是什么?
synchronized和volatile是Java中用于实现多线程同步的关键字,它们的区别如下:
1. 可见性
:volatile关键字用于保证变量的可见性,即当一个线程修改了被volatile修饰的变量的值时,其他线程能够立即看到最新的值。而synchronized关键字不仅保证了可见性,还保证了原子性和有序性。
2. 原子性
:synchronized关键字可以保证代码块或方法的原子性,即同一时间只能有一个线程执行该代码块或方法。而volatile关键字不能保证原子性,它只能保证对单个变量的读取和写入操作是原子的。
3. 顺序性
:synchronized关键字可以保证代码块或方法的执行顺序,即前一个线程释放锁后,后一个线程才能获取锁并执行。而volatile关键字不能保证代码的顺序性。
下面是一个示例,说明synchronized和volatile的使用:
使用synchronized关键字实现同步:
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
在上面的代码中,我们使用synchronized关键字来修饰increment()和getCount()方法,确保对count变量的读写操作是同步的,保证了线程安全。
使用volatile关键字实现可见性:
public class Counter {
private volatile int count;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
在上面的代码中,我们使用volatile关键字来修饰count变量,确保对count变量的读写操作是可见的。当一个线程修改了count的值时,其他线程能够立即看到最新的值。
需要注意的是,尽管volatile关键字可以保证变量的可见性,但它并不能保证原子性和顺序性。在需要同时保证可见性、原子性和顺序性的场景下,应该使用synchronized关键字来实现线程同步。
27、 Java中的ReadWriteLock是什么?
Java中的ReadWriteLock是一种锁机制,用于控制对共享资源的访问。与传统的互斥锁(Mutex)不同,ReadWriteLock支持多个线程同时读取共享资源,但只允许一个线程写入共享资源。
ReadWriteLock接口定义了两个锁对象:读锁(Read Lock)和写锁(Write Lock)。
-
读锁(Read Lock):多个线程可以同时获取读锁,允许并发读取共享资源。当没有线程持有写锁时,读锁可以被多个线程同时持有。
-
写锁(Write Lock):写锁是独占锁,只允许一个线程持有写锁,用于独占地修改共享资源。当有线程持有读锁或写锁时,其他线程请求写锁会被阻塞,直到写锁被释放。
下面是一个使用ReadWriteLock的示例:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class SharedResource {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private int value;
public int readValue() {
lock.readLock().lock();
try {
return value;
} finally {
lock.readLock().unlock();
}
}
public void writeValue(int newValue) {
lock.writeLock().lock();
try {
value = newValue;
} finally {
lock.writeLock().unlock();
}
}
}
在上述示例中,SharedResource类使用ReadWriteLock来控制对value属性的读写访问。readValue()方法获取读锁并读取value的值,多个线程可以同时调用readValue()方法进行读取。writeValue()方法获取写锁并修改value的值,只允许一个线程调用writeValue()方法进行写入。
使用ReadWriteLock可以提高并发性能,因为多个线程可以同时读取共享资源而无需互斥。然而,需要根据具体的场景和需求来选择合适的锁机制,ReadWriteLock适用于读多写少的场景。
28、 什么是重排序?
重排序是指在程序执行过程中,编译器、处理器或运行时系统对指令的执行顺序进行优化或重新排列的过程。重排序可以提高程序的性能和效率,但也可能导致程序的行为变得不可预测或出现错误。
在多线程环境下,重排序可能会引发线程安全问题。由于指令重排序可能改变程序中的数据依赖关系,当多个线程并发执行时,可能会导致结果不一致或出现数据竞争的情况。
为了避免重排序导致的问题,可以使用以下手段:
1. 使用volatile关键字
:在Java中,使用volatile关键字修饰的变量会禁止指令重排序,保证了变量的可见性和有序性。
2. 使用synchronized或Lock
:使用同步机制可以保证多个线程对共享数据的访问具有原子性和有序性,避免了重排序导致的问题。
3. 使用final关键字
:使用final关键字修饰的变量在构造函数完成前不能被重排序,可以保证对象的安全发布。
4. 使用volatile、synchronized、Lock或Atomic类提供的特殊方法
:这些方法提供了特定的内存屏障(Memory Barrier),可以保证在特定位置插入禁止重排序的指令,从而避免了重排序问题。
代码示例:
public class ReorderingExample {
private int x = 0;
private volatile boolean flag = false;
public void write() {
x = 1; // 写入x
flag = true; // 写入flag
}
public void read() {
if (flag) {
System.out.println(x); // 读取x
}
}
}
在上述示例中,如果没有使用volatile关键字修饰flag变量,那么在read()方法中可能会发生指令重排序,导致flag为true,但x仍然为0的情况。使用volatile关键字修饰flag变量可以禁止指令重排序,保证了x的可见性和有序性。
需要注意的是,重排序是一个底层的优化技术,对于大多数应用程序来说,并不需要过多关注和处理重排序问题。只有在特定的多线程环境下,才需要采取相应的措施来保证线程安全和正确性。
29、 为什么 Thread 类的 sleep()和 yield ()方法是静态的?
Thread类的sleep()和yield()方法是静态方法的原因是因为它们不需要依赖具体的线程对象来调用,而是直接作用于当前正在执行的线程。
1. sleep()方法
:sleep()方法用于让当前线程暂停执行一段时间,可以模拟线程的等待或延迟操作。由于它是静态方法,可以直接通过Thread类来调用,而不需要实例化一个Thread对象。例如:
Thread.sleep(1000); // 暂停当前线程1秒钟
2. yield()方法
:yield()方法用于提示线程调度器将当前线程切换出CPU,让其他线程有机会执行。同样地,由于它是静态方法,可以直接通过Thread类来调用。例如:
Thread.yield(); // 提示线程调度器切换当前线程
通过静态方法调用这些方法可以方便地在任何地方使用,而不需要关心具体的线程对象。这样可以简化代码的编写,提高代码的可读性和可维护性。
需要注意的是,sleep()和yield()方法都可能会影响线程的执行顺序和性能,因此在使用时需要谨慎考虑,并根据具体的需求选择合适的场景和方式使用。
30、 请解释StackOverflowError和OutOfMemeryError的区别?
StackOverflowError和OutOfMemoryError是Java中两种常见的错误类型,它们在发生的原因和表现上有所不同。
StackOverflowError(栈溢出错误):
StackOverflowError是在递归调用或者方法调用链过长时发生的错误。当方法调用的深度超过JVM栈的最大深度限制时,就会抛出StackOverflowError。这通常是由于无限递归调用或者方法调用链过长导致的。栈溢出错误是一个严重的错误,它表示程序无法继续执行。
示例:
public class StackOverflowExample {
public static void recursiveMethod() {
recursiveMethod();
}
public static void main(String[] args) {
recursiveMethod();
}
}
上述示例中, recursiveMethod()
方法无限递归调用自身,导致栈溢出错误。
OutOfMemoryError(内存溢出错误):
OutOfMemoryError是在程序申请的内存超过JVM堆内存限制时发生的错误。当JVM无法为新的对象分配内存空间时,就会抛出OutOfMemoryError。这通常是由于程序中存在内存泄漏、大量对象创建或者内存资源不足导致的。内存溢出错误也是一个严重的错误,它表示程序无法继续正常执行。
示例:
import java.util.ArrayList;
import java.util.List;
public class OutOfMemoryExample {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
}
}
上述示例中,不断向List中添加新的对象,导致内存不断增长,最终抛出OutOfMemoryError。
总结:
StackOverflowError是由方法调用链过长或者无限递归调用引起的错误,而OutOfMemoryError是由于程序申请的内存超过JVM堆内存限制或者内存资源不足引起的错误。两者都表示程序无法继续执行,但发生的原因和表现上有所不同。