面试题答案
一键面试1. 特点对比
多线程并发控制
- 轻量级:线程共享进程资源,创建和销毁开销小,切换成本低。锁机制如
synchronized
关键字或java.util.concurrent.locks
包下的锁,操作简单,能快速实现对共享资源的同步访问。信号量Semaphore
可控制同时访问特定资源的线程数量。 - 数据共享方便:同一进程内线程共享内存空间,数据交互容易,但也正因如此,需更谨慎处理数据一致性问题。
进程并发控制
- 重量级:进程拥有独立的内存空间,创建和销毁开销大,进程间切换成本高。通过管道、消息队列、共享内存、信号量、套接字等IPC(Inter - Process Communication)机制实现进程间通信与并发控制。
- 数据隔离性好:每个进程有独立内存,数据安全性高,一个进程崩溃一般不影响其他进程,但进程间通信相对复杂。
2. 适用场景对比
多线程适用场景
- 计算密集型任务:如复杂的数值计算,多个线程可利用多核CPU并行计算,提高计算效率。例如,矩阵乘法的并行计算,不同线程负责矩阵不同部分的乘法运算。
- I/O密集型任务:在等待I/O操作(如网络请求、文件读取)时,线程可释放CPU资源去执行其他任务,提高系统整体效率。如网络爬虫程序,多个线程可同时发起不同网页的请求。
进程适用场景
- 需要高可靠性的场景:由于进程间相互独立,一个进程出现故障不会影响其他进程,适用于关键服务,如数据库服务器的不同服务模块采用进程方式部署。
- 跨机器分布式场景:进程间通过套接字等方式可方便实现跨机器通信,用于构建分布式系统,如分布式文件系统中不同节点以进程形式运行并相互协作。
3. 可能出现的问题及避免方法
多线程
- 死锁问题:多个线程相互等待对方释放锁,导致程序卡死。
- 避免方法:破坏死锁的四个必要条件(互斥、占有并等待、不可剥夺、循环等待)。例如,按顺序加锁,所有线程获取锁的顺序一致;设置超时时间,线程获取锁超过一定时间则放弃。
- 示例:
class DeadlockExample {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("Thread 2 acquired lock2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
});
thread1.start();
thread2.start();
}
}
通过调整加锁顺序可避免死锁,如让两个线程都先获取lock1
再获取lock2
。
- 数据竞争问题:多个线程同时读写共享数据导致数据不一致。
- 避免方法:使用同步机制,如
synchronized
关键字、Lock
接口等对共享数据的访问进行同步。 - 示例:
- 避免方法:使用同步机制,如
class DataRaceExample {
private static int counter = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
synchronized (DataRaceExample.class) {
counter++;
}
}
});
threads[i].start();
}
for (Thread thread : threads) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Final counter value: " + counter);
}
}
进程
- 同步与通信复杂问题:进程间通信机制相对复杂,使用不当易导致数据传输错误或同步问题。
- 避免方法:使用成熟的IPC框架,如
Apache Mina
等,简化进程间通信与同步逻辑。仔细设计通信协议,确保数据的准确传输和同步。 - 示例:在使用共享内存进行进程间通信时,要通过信号量等机制确保一个进程写入数据时,其他进程不会同时读取或写入,防止数据损坏。
- 避免方法:使用成熟的IPC框架,如
// 简单的共享内存和信号量示例(C语言,假设在Linux环境)
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#define SHM_SIZE 1024
int main() {
key_t key = ftok(".", 'a');
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
exit(1);
}
char *shared_memory = (char *)shmat(shmid, NULL, 0);
if (shared_memory == (void *)-1) {
perror("shmat");
exit(1);
}
sem_t *sem = sem_open("/mysem", O_CREAT, 0666, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(1);
}
// 写入数据前获取信号量
sem_wait(sem);
sprintf(shared_memory, "Hello from process");
// 写入数据后释放信号量
sem_post(sem);
sem_close(sem);
sem_unlink("/mysem");
shmdt(shared_memory);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
- 资源消耗问题:进程创建和维护开销大,过多进程会导致系统资源耗尽。
- 避免方法:合理规划进程数量,根据系统资源(如内存、CPU)情况动态调整进程数。例如,使用进程池技术,复用已创建的进程,减少进程创建和销毁开销。