面试题答案
一键面试可能出现的问题
- 线程安全问题:
- 添加和移除观察者时:在多线程环境下,当多个线程同时对观察者列表进行添加或移除操作时,可能会出现数据不一致。例如,线程A正在添加一个观察者,同时线程B试图移除该观察者,这可能导致观察者列表处于不一致状态。
- 通知观察者时:如果在通知观察者时,观察者的处理逻辑不是线程安全的,可能会引发问题。比如多个线程同时更新被观察对象的状态,然后通知观察者,观察者在处理更新时可能会出现数据竞争。
- 数据一致性问题:
- 被观察对象状态更新和通知观察者的时机不同步。例如,被观察对象的状态已经更新,但由于线程调度等原因,观察者没有及时收到更新通知,导致观察者使用的是旧的数据,从而产生数据不一致。
举例说明
假设我们有一个股票价格监控系统,股票价格是被观察对象,不同的投资者是观察者。
import java.util.ArrayList;
import java.util.List;
// 被观察对象
class StockPrice {
private double price;
private List<Investor> investors = new ArrayList<>();
public void attach(Investor investor) {
investors.add(investor);
}
public void detach(Investor investor) {
investors.remove(investor);
}
public void setPrice(double price) {
this.price = price;
notifyInvestors();
}
private void notifyInvestors() {
for (Investor investor : investors) {
investor.update(price);
}
}
}
// 观察者
class Investor {
private String name;
public Investor(String name) {
this.name = name;
}
public void update(double price) {
System.out.println(name + " 收到股票价格更新: " + price);
}
}
在多线程环境下,假设有多个线程同时操作StockPrice
对象的attach
、detach
和setPrice
方法,就可能出现上述线程安全和数据一致性问题。
解决思路及代码实现
- 线程安全问题解决:
- 使用线程安全的集合:将观察者列表改为
CopyOnWriteArrayList
,它在修改时会创建一个新的数组,读操作不会受写操作影响,从而保证线程安全。 - 同步方法:对关键方法(如
attach
、detach
和notifyInvestors
)使用synchronized
关键字进行同步。
- 使用线程安全的集合:将观察者列表改为
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
// 被观察对象
class StockPrice {
private double price;
private List<Investor> investors = new CopyOnWriteArrayList<>();
public synchronized void attach(Investor investor) {
investors.add(investor);
}
public synchronized void detach(Investor investor) {
investors.remove(investor);
}
public synchronized void setPrice(double price) {
this.price = price;
notifyInvestors();
}
private void notifyInvestors() {
for (Investor investor : investors) {
investor.update(price);
}
}
}
// 观察者
class Investor {
private String name;
public Investor(String name) {
this.name = name;
}
public void update(double price) {
System.out.println(name + " 收到股票价格更新: " + price);
}
}
- 数据一致性问题解决:
- 使用阻塞队列:可以在被观察对象和观察者之间使用阻塞队列。当被观察对象状态更新时,将更新信息放入阻塞队列,观察者从阻塞队列中获取更新信息,这样可以保证观察者获取到的是最新的数据。
- 使用
CountDownLatch
或CyclicBarrier
:在通知观察者之前,使用CountDownLatch
或CyclicBarrier
来确保所有必要的更新操作都已完成,然后再通知观察者。例如,在多个线程更新被观察对象状态后,使用CountDownLatch
等待所有更新线程完成,再通知观察者。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
// 被观察对象
class StockPrice {
private double price;
private BlockingQueue<Double> priceQueue = new LinkedBlockingQueue<>();
private List<Investor> investors = new CopyOnWriteArrayList<>();
public synchronized void attach(Investor investor) {
investors.add(investor);
}
public synchronized void detach(Investor investor) {
investors.remove(investor);
}
public synchronized void setPrice(double price) {
this.price = price;
try {
priceQueue.put(price);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
notifyInvestors();
}
private void notifyInvestors() {
for (Investor investor : investors) {
new Thread(() -> {
try {
double newPrice = priceQueue.take();
investor.update(newPrice);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
// 观察者
class Investor {
private String name;
public Investor(String name) {
this.name = name;
}
public void update(double price) {
System.out.println(name + " 收到股票价格更新: " + price);
}
}
通过上述方法,可以在多线程场景下较好地解决Java观察者模式中的线程安全和数据一致性问题。