使用过程概述:
- 被观察者(股票)继承java.util.Observable[可观察的]类
- 观察者(炒股人)实现java.util.Observer接口
- 被观察者实现自身业务逻辑(股票涨跌),指定什么时候发起通知notifyObervers()
- 观察者实现接口方法update(),指定被通知时的动作
- 被观察者实例调用addObserver()方法选择观察自己的观察者
简单实例:
import java.util.Observable;
import java.util.Observer;
public class ObserverTest {
public static void main(String[] args) {
Share share1 = new Share("茅台");
Share share2 = new Share("大宝"); //定义两种股票,三个炒股者
ShareHolder holder1=new ShareHolder("王富贵"),holder2 = new ShareHolder("张思睿"),
holder3 = new ShareHolder("熊初墨");
share1.addObserver(holder1);share1.addObserver(holder2); //12买入茅台
share2.addObserver(holder2);share2.addObserver(holder3); //23买入大宝
share1.increase(); //茅台涨
share2.decrease(); //大宝跌
}
}
//被观察者继承java.util.Observable[可观察的]类
class Share extends Observable{
String name;
Share(String name){
this.name=name;
}
public void decrease() { //动作一,股票跌了
setChanged(); //标记被观察者已有变化
notifyObservers(name+"跌了!"); //通知观察者变化
}
public void increase() { //动作二
setChanged();
notifyObservers(name+"涨了!"); //参数是向update()传去的值
}
}
//观察者实现java.util.Observer接口并实现接口方法update()
class ShareHolder implements Observer{
String name;
ShareHolder(String name){
this.name=name;
}
@Override
public void update(Observable o, Object arg) { //当观察者收到变化时执行
if(arg.toString().indexOf("涨")==-1)
System.out.println("Poor "+name+", "+arg);
else
System.out.println("Lucky "+name+", "+arg);
}
}
- 通过实例可以看出,观察者和被观察者是多对多的关系。
- 被观察者相当于自变量,自变量怎样以及何时发生变化根据业务逻辑设定,如上股票涨跌时变化。这个变化通过setChanged()+notifyObserver()方法通知到每一个观察者
- 观察者重写update()方法根据notifyObserver(arg)传递来的参数决定动作
方法实现:
Observer(观察者)接口只有一个抽象方法invoke(),这里的重点是Observable,即被观察者类。下面的代码来自jdk8源码,去掉了注释并将方法做了重新排序
public class Observable {
private boolean changed = false;
protected synchronized void setChanged() {
changed = true;
}
protected synchronized void clearChanged() {
changed = false;
}
public synchronized boolean hasChanged() {
return changed;
}
如上,changed()是被观察者类第一个实例变量,方法很简单。所有的方法都通过同步锁确保并发安全
被观察者类的第二个实例变量是一个集合,线程安全的Vector,存放了观察这个被观察者类的对象。基本就是CRUD
private Vector<Observer> obs;
public Observable() {
obs = new Vector<>();
}
public synchronized void addObserver(Observer o) {
if (o == null)
throw new NullPointerException();
if (!obs.contains(o)) {
obs.addElement(o); //"确保一个用户只能对特定公众号订阅一次,即不可重入"
}
}
public synchronized void deleteObserver(Observer o) {
obs.removeElement(o);
}
public synchronized void deleteObservers() {
obs.removeAllElements();
}
public synchronized int countObservers() {
return obs.size();
}
并且所有的方法仍然加了同步锁。注意add()方法
最关键的代码是notifyObservers:
public void notifyObservers() {
notifyObservers(null);
}
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
}
notifyObservers()方法的线程安全设计:
如果是自己要实现notifyOs()方法会怎样设计?
版本1,没有考虑线程安全时可能会设计成这样:
public void notifyObservers(Object o){
if(!changed())
return ;
clearChanged();
for(Observer o : obs)
o.update();
}
存在的问题:
如果被观察者同时被调用两次会怎么样?比如股票刚涨了立刻又跌了,炒股者会不会只收到一次消息?
线程1: inc(){setChanged(); notifyOs();} 线程2:dec(){setChanged(); notifyOs();}
假设线程1执行setChanged()之后,线程2执行setChanged().
然后线程1执行notifyOs(),此时线程1会将changed域变量标记为false,等到线程2执行notifyOs()方法时将直接退出。dec这个变化将不会被观察者订阅到
即第一个问题是多线程环境下的“丢失更改”
第二个问题,在某次notifyOs()方法执行时,假设有新的用户订阅或者老用户取消订阅,会直接报错。因为遍历一个集合时不可以同时对其进行增删改操作。如下
public static void main(String[] args) throws InterruptedException {
final Vector<String> vec = new Vector<>();
vec.add("考");vec.add("研");vec.add("加");vec.add("油");
new Thread(()->vec.add(new String("新年快乐"))).start(); //子线程添加
for(String s : vec)
System.out.print(s); //主线程遍历
}
版本2,考虑线程安全时:
public void notifyObservers(Object o){
synchronized(this){
if(!changed())
return ;
clearChanged();
for(Observer o : obs)
o.update();
}
}
考虑原问题1:
线程1: inc(){setChanged(); notifyOs();} 线程2:dec(){setChanged(); notifyOs();}
假设线程1执行setChanged()之后,线程2执行setChanged().
然后线程1执行notifyOs(),此时线程1会将changed域变量标记为false,等到线程2执行notifyOs()方法时将直接退出。dec这个变化将不会被观察者订阅到
即问题1仍然存在
问题2已经解决,在notifyObservers()中直接锁定了this即锁定集合,此时将阻塞增删改操作自然也就安全了。不过这样效率极低。将会出现在执行notifyOs()方法时新用户无法订阅的现象。而这是万万不可的
回过头来看官方的设计
public void notifyObservers(Object arg) {
Object[] arrLocal;
synchronized (this) {
if (!changed)
return;
arrLocal = obs.toArray();
clearChanged();
}
for (int i = arrLocal.length-1; i>=0; i--)
((Observer)arrLocal[i]).update(this, arg);
}
通过toArray()的方式实现深拷贝,记录此时的快照,高效解决问题二
问题1
线程1: inc(){setChanged(); notifyOs();} 线程2:dec(){setChanged(); notifyOs();}
假设线程1执行setChanged()之后,线程2执行setChanged().
然后线程1执行notifyOs(),此时线程1会将changed域变量标记为false,等到线程2执行notifyOs()方法时将直接退出。dec这个变化将不会被观察者订阅到
问题1还是存在??是的,一直存在
此处用代码给出官方设计不能解决问题1的例子:
首先将观察者改成如下
class ShareHolder implements Observer{
String name;
ShareHolder(String name){
this.name=name;
}
static volatile int count=0; //保持可见性
@Override
public void update(Observable o, Object arg) { //每通知一个观察者,count+1
++count;
}
}
修改Main方法:
for(int i=0;i<1000;i++)
new Thread(()-> {
share1.increase(); //share1有两个用户订阅,执行1000次
}).start();
while(Thread.activeCount()>1); //等待线程执行完毕
System.out.println(ShareHolder.count); //输出计数器
由于share1有两个用户订阅,如果说上述操作线程安全,最终计数器的值将会是2000.而实际上:
实际上,问题1不是Observer能够解决的问题,它只能保证自己的内部操作合法,好比vector是一个线程安全的类,但我们上面遍历的同时增删改仍会报错一样。
要解决问题一也很简单。比如一种简单粗暴的解决方式,在Share的inc()和dec()方法上加上sync
Deprecated:
原始的观察者被观察者在jdk9开始已经被标记为@Deprecated(since="9")
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/Observable.html
备注:
因为工作中遇到了这样的代码设计,所以开始在优快云参考他人博文。但是我确实忘记了参考的是哪一篇了。
因为后来自己看源码分析后发现原博文对于线程的考虑有错误的地方(他认为问题一二都已解决)。所以就自己写了篇。
总感觉这种设计模式听起来像是发布订阅。。不过我还不知道什么是发布订阅~~~