Java作业-12
java多线程
Buffer 类中的 read 和write 方法可以并行执行吗?
在一般情况下,如果没有适当的同步机制,read 和 write 操作可能会并行执行。这可能导致竞态条件,如两个线程同时读或写,可能会导致数据不一致或损坏。因此,通常需要通过同步机制(如 synchronized 关键字、锁或其他并发控制工具)来确保线程安全。
调用 read 方法时,如果队列为空会发生什么?
当队列为空时,尝试执行 read 操作通常会导致线程阻塞或等待,直到队列中有数据可读。这种行为是典型的生产者-消费者问题中的“消费者”行为。在 Java 中,这可以通过 wait() 和 notify() 方法来实现,或使用更高级的并发工具,如 BlockingQueue。
调用 write 方法时,如果队列满了会发生什么?
当队列满时,尝试执行 write 操作通常会导致线程阻塞或等待,直到队列中有足够的空间。这也是生产者-消费者问题中的一种典型情况。类似于 read 操作,这可以通过使用 wait() 和 notify() 或 BlockingQueue 等机制来处理。
什么是阻塞队列?Java中支持什么阻塞队列?
阻塞队列(Blocking Queue)是一种支持两个附加操作的队列:当队列为空时,获取元素的线程将会被阻塞,直到队列中有新的元素可取;当队列满时,存储元素的线程将会被阻塞,直到队列中有空间可用。阻塞队列是一个非常有用的数据结构,用于生产者-消费者问题,它可以安全地管理多个线程之间的数据交换。
| 队列类型 | 描述 |
|---|---|
ArrayBlockingQueue |
基于数组结构的有界阻塞队列 |
LinkedBlockingQueue |
基于链表结构的可选有界阻塞队列(默认和最大容量为 Integer.MAX_VALUE) |
PriorityBlockingQueue |
支持优先级排序的无界阻塞队列 |
DelayQueue |
使用元素的延迟属性来控制元素的可用性的无界阻塞队列 |
SynchronousQueue |
不存储元素的阻塞队列,每个插入操作必须等待一个移除操作,反之亦然 |
LinkedTransferQueue |
一个由链表结构组成的无界阻塞队列 |
LinkedBlockingDeque |
基于链表结构的可选有界双端阻塞队列 |
如何创建一个允许3个并行线程的信号量?如何获取一个信号量?如何释放一个信号量?
// 创建一个初始许可数为3的 Semaphore 实例。这表示最多允许3个线程同时持有许可。
Semaphore semaphore = new Semaphore(3);
// 当一个线程想要访问共享资源时,它必须先从信号量获取许可。如果信号量的计数大于零,线程将获得许可,并且信号量的计数将减少。如果计数为零,则当前线程将阻塞,直到另一个线程释放许可。
semaphore.acquire();
// 或者,为了避免潜在的阻塞,可以使用:
boolean success = semaphore.tryAcquire();
// 这将立即返回,success 为 true 表示获取许可成功,为 false 表示获取失败。
// 当线程完成对共享资源的访问后,它应该释放信号量。这会增加信号量的计数,并可能允许等待的其他线程获得许可
semaphore.release();
锁和信号量之间的相似之处和不同之处在什么地方?
什么是死锁?如何避免死锁?
死锁是指两个或多个线程在执行过程中,因为争夺资源而陷入相互等待的状态,导致它们都无法继续执行的现象。在死锁状态下,每个线程都在等待一个被其他线程持有的资源。
死锁通常涉及两种类型的资源:
- 互斥资源:不能被多个线程同时使用的资源。
- 占有并等待:一个线程至少持有一个资源且等待获取额外的资源。
死锁的四个必要条件: - 互斥条件:至少有一个资源必须处于非共享模式,即一次只有一个线程能使用资源。
- 占有并等待:一个线程至少占有一个资源,并等待获取其他线程占有的资源。
- 不可抢占条件:资源只能由占有它们的线程自愿释放。
- 循环等待条件:存在一种线程资源的循环等待关系。
避免死锁的策略: - 打破互斥条件:尽可能设计算法和数据结构,使多个线程可以共享资源,从而避免互斥。
- 打破占有并等待:一次性分配所有资源,避免在持有其他资源的情况下等待额外资源。
- 打破不可抢占条件:允许线程抢占其他线程持有的资源。
- 打破循环等待条件:实施资源排序分配策略,强制每个线程以预定的顺序申请资源。
实际操作建议: - 资源分配顺序:定义资源的固定顺序,并按此顺序申请资源,避免循环等待。
- 使用锁超时:使用带超时的锁,避免线程永久持有锁。
- 死锁检测:定期检查和识别死锁,一旦检测到死锁,采取措施打破之。
- 减少资源占用:尽量减少每个线程所需资源的数量和持有时间,以降低发生死锁的风险。
- 使用并发控制工具:利用现代编程语言提供的高级并发控制工具,如 Java 中的 java.util.concurrent 包提供的锁。
什么是线程状态?描述一个线程的状态
| 状态 | 描述 |
|---|---|
NEW |
线程刚被创建,但还没有调用 start() 方法。 |
RUNNABLE |
线程正在 Java 虚拟机中执行,但它可能正在等待来自操作系统的资源(如处理器)。 |
BLOCKED |
线程被阻塞,等待监视器锁定,以进入或重新进入同步块/方法。 |
WAITING |
线程无限期等待其他线程来执行特定操作,例如 Object.wait、Thread.join 或 LockSupport.park。 |
TIMED_WAITING |
线程在指定的最大时间内等待另一个线程来执行特定操作,例如 Thread.sleep、Object.wait 带超时、Thread.join 带超时或 LockSupport.parkNanos。 |
TERMINATED |
线程已经执行完毕。 |
什么是同步合集?ArrayList是同步的吗?如何使得其同步?
(演示ConcurrentModificationException异常)选代器具有快速失效特性,编写一个程序来演示该特性,创建两个并发访问和修改集合的线程。第一个线程创建一个用数填充的散列集并每秒钟向该合集内添加一个新的数。第二个线程获取上述合集的一个迭代器,并通过该迭代器每秒前后遍历一次合集。因为第二个线程遍历合集时,第一个线程正在修改合集,所以,会得到一个ConcurrentModificationException异常
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class ConcurrentModificationDemo {
public static void main(String[] args) {
Set<Integer> hashSet = Collections.synchronizedSet(new HashSet<>());
// 线程1:向集合中添加元素
Thread thread1 = new Thread(() -> {
int count = 0;
while (true) {
try {
Thread.sleep(1000);
hashSet.add(count++);
System.out.println("Added: " + (count - 1));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 线程2:遍历集合
Thread thread2 = new Thread(() -> {
try {
while (true) {
Thread.sleep(1000);
Iterator<Integer> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println("Reading: " + iterator.next());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ConcurrentModificationException e) {
System.out.println("ConcurrentModificationException caught!");
}
});
thread1.start();
thread2.start();
}
}
(使用同步合集)使用同步解决前一个练习题中的问题,使得第二个线程不抛出 Concurrent-ModificationException 异常
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class SynchronizedCollectionDemo {
public static void main(String[] args) {
Set<Integer> hashSet = new HashSet<>();
// 线程1:向集合中添加元素
Thread thread1 = new Thread(() -> {
int count = 0;
while (true) {
try {
Thread.sleep(1000);
synchronized (hashSet) {
hashSet.add(count++);
System.out.println("Added: " + (count - 1));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
// 线程2:同步遍历集合
Thread thread2 = new Thread(() -> {
while (true) {
try {
Thread.sleep(1000);
synchronized (hashSet) {
Iterator<Integer> iterator = hashSet.iterator();
while (iterator.hasNext()) {
System.out.println("Reading: " + iterator.next());
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
thread1.start();
thread2.start();
}
}
(演示死锁)编写一个演示死锁的程序
public class DeadlockDemo {
public static void main(String[] args) {
final Object resource1 = new Object();
final Object resource2 = new Object();
// 线程1尝试先锁定资源1,然后资源2
Thread thread1 = new Thread(() -> {
synchronized (resource1) {
System.out.println("Thread 1: Locked resource 1");
try {
Thread.sleep(50); // 让线程2有机会锁定资源2
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource2) {
System.out.println("Thread 1: Locked resource 2");
}
}
});
// 线程2尝试先锁定资源2,然后资源1
Thread thread2 = new Thread(() -> {
synchronized (resource2) {
System.out.println("Thread 2: Locked resource 2");
try {
Thread.sleep(50); // 让线程1有机会锁定资源1
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (resource1) {
System.out.println("Thread 2: Locked resource 1");
}
}
});
thread1.start();
thread2.start();
}
}
(并行数组初始化器)使用 Fork/Join 框架实现下面的方法,可以设置随机值给线性表。public static void parallelAssignValues(double[] list)编写一个测试程序,创建一个具有9000 000个元素的线性表,调用 paralleAssignValues来赋随机值给线性表。另外实现一个顺序算法,并且比较两种方法执行的时间。注意,如果使用Math.random(),并行代码的执行时间将比顺序代码的执行时间差,因为 Math.random()是同步的,不能并行执行。为了解决这个问题,创建一个 Random 对象,用于赋随机值给一个小的线性表
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ThreadLocalRandom;
public class ParallelArrayInitializer {
// 继承 RecursiveAction 来创建可以用于 Fork/Join 框架的任务
static class AssignTask extends RecursiveAction {
private final double[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000; // 分割阈值
AssignTask(double[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected void compute() {
if (end - start < THRESHOLD) {
for (int i = start; i < end; i++) {
array[i] = ThreadLocalRandom.current().nextDouble();
}
} else {
int middle = start + (end - start) / 2;
invokeAll(new AssignTask(array, start, middle), new AssignTask(array, middle, end));
}
}
}
// 并行赋值方法
public static void parallelAssignValues(double[] list) {
ForkJoinPool pool = new ForkJoinPool();
pool.invoke(new AssignTask(list, 0, list.length));
}
// 顺序赋值方法
public static void sequentialAssignValues(double[] list) {
for (int i = 0; i < list.length; i++) {
list[i] = Math.random();
}
}
// 测试方法
public static void main(String[] args) {
double[] list = new double[9000000];
// 测试并行赋值
long startTime = System.currentTimeMillis();
parallelAssignValues(list);
long endTime = System.currentTimeMillis();
System.out.println("Parallel time: " + (endTime - startTime) + " ms");
// 测试顺序赋值
startTime = System.currentTimeMillis();
sequentialAssignValues(list);
endTime = System.currentTimeMillis();
System.out.println("Sequential time: " + (endTime - startTime) + " ms");
}
}