生产者和消费者问题一直是多线程和同步代码块知识点的一个考察区域,是指分别有两条线程同时操作一个线性表,当线性表满的时候停止生产,当线性表为空的时候停止消费。
笔者在实际运用过程中,需要根据需求实现一个相对简易的生产消费问题。比如说在直播应用中,弹幕系统就需要一个这样的情形,随机的收到弹幕消息,但是弹幕只能每隔一秒(或者其他固定时间)放出一条,不然就会造成界面重叠等情况,所以我们根据需求有如下技术实现。
我们有一个线程安全的线性表,当表里有数据的时候,每隔固定时间都从表里拿出最前面一个元素,发送到UI线程进行处理,向线性表中添加数据的频率则是不固定的,随机的添加数据到表的尾部,一次添加一个,我们希望线性表size为0的时候,取操作(消费操作)暂停运行,当插入数据后检查线性表长度,发现线性表的size=1时,恢复取操作(消费操作)。
接下来我们来模拟上面的情况。
既然是生产消费问题,那么Thread类肯定是少不了了,程序开始运行之后,即开始运行消费者线程类Consumer#run
,在其内部的死循环中每次sleep(2000)
使其暂停两秒钟。
消费者内部逻辑:当检测到ArrayList中有元素时,取出下标为0的元素,通过Handler发送到UI线程并打印出来。如果没有元素,直接调用线性表的wait
方法。
生产者逻辑:每点击一次Hello World按钮,都向ArrayList中添加一个元素。
下面我们看具体代码。
private static class Consumer extends Thread {
private final LinkedList<Integer> arrayList;
private MyHandler myHandler;
private boolean flag = true;
//下面是第七行
Consumer(MyHandler myHandler, LinkedList<Integer> arrayList) {
this.arrayList = arrayList;
this.myHandler = myHandler;
}
@Override
public void run() {
super.run();
while (flag) {
while (arrayList.isEmpty()) {
try {
synchronized (arrayList) {
arrayList.wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Integer integer = arrayList.remove();
sendMessage(integer);
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void setFlag(boolean flag) {
this.flag = flag;
}
private void sendMessage(Integer integer) {
Log.v("Consumer", "sendMessage");
Message message = Message.obtain();
message.obj = String.valueOf(integer);
myHandler.sendMessage(message);
}
}
为了避免内存泄漏和其他的一些狗屁问题,我们将这个消费者线程类(也是内部类),写成了静态私有类。在第七行的构造方法中,我们传入了一个我们自己改造过的handler类和线性表以供操作。
在run
方法中,为了进行持续操作,写入了一个while死循环,flag在初始化的时候被设置为true,当你调用setflag
方法,这个消费者类的工作也就结束了,我会在UI层面的onDestory
方法中这么做。再往内部,首先判断当前的arrayList是否为空,如果为空,则将进程共享的线性表调用Object#wait
方法,达到消费暂停的效果。当然,我们的判断是while而不是if,这是wait
推荐被调用的手段之一,同时内部也不要忘了synchronized来获取锁。
再往下就是真正的消费操作了,取出线性表的最后一个元素,使用handler类来发送到主线程。然后让消费线程类睡眠2000毫秒。
private void startEvent() {
consumer = new Consumer(myHandler, arrayList);
consumer.start();
//PRODUCE
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
arrayList.add(number++);
if (arrayList.size() == 1)
synchronized (arrayList) {
arrayList.notifyAll();
}
}
});
}
再来看看我们的生产操作。每点击一次button,都向线性表中做了一次添加操作,同时在线性表为空然后添加元素之后,对线性表做了notifyAll
操作。
这样的生产消费模型,使我们我们真正的能做到在没有元素的时候使得Thread类处于阻塞状态。
private static class MyHandler extends Handler {
private WeakReference<MainActivity> weakReference;
MyHandler(MainActivity activity) {
weakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
String currentText = weakReference.get().textView.getText().toString();
weakReference.get().textView.setText(currentText + "\n" + msg.obj);
weakReference.get().scrollView.fullScroll(ScrollView.FOCUS_DOWN);
}
}
自定义Handler类,同样使用了静态和私有手段去处理……还添加了弱引用。
@Override
protected void onDestroy() {
super.onDestroy();
consumer.setFlag(false);
myHandler.removeCallbacksAndMessages(null);
}
当然,也不要忘记在UI结束的时候结束消费者的运行。
写到这个地方,我还是有一些疑问:
1:因为在点击事件中,并没有将涉及有arrayList的操作全部包含到同步代码块中,在这种情况下线性表真的线程安全吗?
2:在消费线程类中,如果把同步代码块的范围扩大到死循环的整个范围,因为sleep的关系会造成button的点击事件受到阻塞,如何解决?