最近操作系统布置了一次多道同步互斥程序编程设计体验的作业,抱着水一水心态的我试了一下用GUI界面实现生产者消费者问题的模拟,发现根本做不出来,同专业的同学有用VS MFC做的,有用C#做的,也有人用Eclipse做的Web项目,还有用JAVA AWT做的,可能只有我用Java Swing做的。后来才知道AWT有插件,才明白他们不懂我手动排版的痛,太可怕了。
言归正传,这次项目设计也用了一周的时间,希望能给大家有一些感悟。
这个类的构造函数主要完成大的控件的添加和摆放,完成整个窗体轮廓的构造,具体的实现部分将在后面介绍。
// An highlighted block
public PC() {
JFrame f = new JFrame("生产者消费者模拟");
Font font = new Font("楷体", Font.PLAIN, 16);
JMenuBar menuBar = new JMenuBar();
JMenu menuFile = new JMenu("文件");
JMenu menuHelp = new JMenu("帮助");
JMenuItem menuFileExit = new JMenuItem("退出");
JMenuItem menuHelpAbout = new JMenuItem("关于");
JMenuItem menuHelpIndution = new JMenuItem("介绍");
// mp.setVisible(true);
Content content = new Content();
f.add("Center", content);
content.setSize(300, 500);
Control control = new Control(content);
f.add("North", control);
}
JPanel是Java Swing中的面板容器,可以容纳各种各样的控件,但是因为它是中间层容器,所以需要依附顶层容器JFrame生存。JPanel的功能十分强大,除了常用的一些方法外,在这次开发中最有用的就是paint()和repaint(),对Paint函数重写,并对界面进行不断的刷新,可以实现简单动画的效果,相信如果用Swing做小游戏,这个功能应该能派上很大的用场。
class Content extends JPanel {// content是主显示部分
JTextArea ta;
Image image;
Image imageP;
Image imageC;
public Content() {
setLayout(null);
Font f = new Font("楷体", Font.PLAIN, 16);
this.setOpaque(false);
ta = new JTextArea();
JScrollPane sp = new JScrollPane(ta);
add(sp);
sp.setBounds(new Rectangle(380, 50, 400, 350));
ta.setLineWrap(true);
producer=new JLabel("生产者");
producer.setBounds(15, 40, 50, 50);
producer.setFont(f);
add(producer);
consumer=new JLabel("消费者");
consumer.setBounds(270, 40, 50, 50);
consumer.setFont(f);
add(consumer);
buffer=new JLabel("缓冲区");
buffer.setBounds(150, 40, 50, 50);
buffer.setFont(f);
add(buffer);
JLabel lable=new JLabel(new Smile().i);
lable.setBounds(30, 300, 70, 70);
add(lable);
}
//动图演示操作
@Override
public void paint(Graphics g) {
super.paint(g);
int y = 100;
int x = 100;
Toolkit tool = this.getToolkit();
image = tool.getImage("img/food.jpg");
imageP = tool.getImage("img/laoban.png");
imageC = tool.getImage("img/maidou.png");
if (Storage.Pdoing == 1) {
g.drawImage(imageP, x - 100 + 60, y, imageP.getWidth(this), imageP.getHeight(this), this);
} else {
g.drawImage(imageP, x - 100, y, imageP.getWidth(this), imageP.getHeight(this), this);
}
if (Storage.Cdoing == 1) {
g.drawImage(imageC, x + 150 - 60, y, imageC.getWidth(this), imageC.getHeight(this), this);
} else {
g.drawImage(imageC, x + 150, y, imageC.getWidth(this), imageC.getHeight(this), this);
}
for (int i = 0; i < Storage.Scount; i++) {
g.drawImage(image, x + 40, y, image.getWidth(this), image.getHeight(this), this);
y = y + image.getHeight(this);
}
}
这个JPanel主要实现对界面的控制,也是这个项目的核心代码所在,创建对几个按钮和复选框的监听事件,实现按钮的动作,这里监听事件的原理和其他图形化编程的原理大同小异。使用getSelectedItem()函数实现对控件参数的返回,从而为后面处罚的事件提供参数。
class Control extends JPanel implements ActionListener {// control是控制选择的部分
Font f = new Font("楷体", Font.PLAIN, 16);
JLabel label1 = new JLabel("生产者个数:");
JComboBox cmb1 = new JComboBox();
JLabel label2 = new JLabel("消费者个数:");
JComboBox cmb2 = new JComboBox();
JLabel label3 = new JLabel("Buffer容量:");
JComboBox cmb3 = new JComboBox();
JLabel label4 = new JLabel("所做操作次数:");
JComboBox cmb4 = new JComboBox();
JButton start = new JButton("开始");
JButton clear = new JButton("清空");
JButton stopButton = new JButton("停止");
Content content;
// JLabel image=new JLabel();
// Icon icon=new ImageIcon("img/food.jpg");
public Control(Content content) {
this.content = content;
cmb1.addItem(1);
cmb1.addItem(2);
cmb1.addItem(3);
cmb1.addItem(4);
cmb2.addItem(1);
cmb2.addItem(2);
cmb2.addItem(3);
cmb2.addItem(4);
cmb3.addItem(1);
cmb3.addItem(2);
cmb3.addItem(3);
cmb3.addItem(4);
cmb4.addItem('1');
cmb4.addItem('N');
label1.setFont(f);
label2.setFont(f);
label3.setFont(f);
label4.setFont(f);
cmb1.setFont(f);
cmb2.setFont(f);
cmb3.setFont(f);
cmb4.setFont(f);
start.setFont(f);
clear.setFont(f);
stopButton.setFont(f);
add(label1);
add(cmb1);
add(label2);
add(cmb2);
add(label3);
add(cmb3);
add(label4);
add(cmb4);
add(start);
add(clear);
add(stopButton);
// image.setIcon(icon);
// image.setBounds(300, 300, icon.getIconWidth(), icon.getIconHeight());
// add(image);
}
public void actionPerformed(ActionEvent e) {// 四个组件的监听事件
int temp1 = (int) cmb1.getSelectedItem();
int temp2 = (int) cmb2.getSelectedItem();
int temp3 = (int) cmb3.getSelectedItem();
char temp4 = (char) cmb4.getSelectedItem();
if (e.getActionCommand() == "开始") {
content.clear();
if (temp4 == '1') {// 选择一个生产者一个消费者一次操作
PC.stop = 1;
go1();
} else if (temp1 == 1 && temp2 == 1 && temp3 == 1 && temp4 == 'N') {// 选择一个生产者一个消费者多次操作
PC.stop = 0;
go2();
} else if (temp1 == 1 && temp2 == 1 && temp3 != 1 && temp4 == 'N') {// 选择一个生产者一个消费者多次操作且Buffer为n
PC.stop = 0;
go3(temp3);
} else if (temp1 != 1 && temp2 != 1 && temp3 != 1 && temp4 == 'N') {// 选择多个生产者多个消费者多次操作且Buffer为n
PC.stop = 0;
go4(temp3,temp1,temp2);
}
} else if (e.getActionCommand() == "清空") {
content.clear();
} else if (e.getActionCommand() == "停止") {
PC.stop = 1;
} else {
// System.out.println("Input Error!");
}
}
public void go1() {// 一个生产者一个消费者的运行程序
Storage storage1 = new Storage();
Thread p1 = new Thread(new Producer(storage1));
Thread c1 = new Thread(new Consumer(storage1));
p1.start();
c1.start();
/*
* System.out.println("【生产者Thread-1】生产一个产品,现库存1");
* System.out.println("【消费者Thread-2】消费一个产品,现库存0");
*/
}
}
}
生产者消费者问题(Producer-consumer problem),也称有限缓冲问题(Bounded-buffer problem),是一个多线程同步问题的经典案例。生产者生成一定量的数据放到缓冲区中,然后重复此过程;与此同时,消费者也在缓冲区消耗这些数据。生产者和消费者之间必须保持同步,要保证生产者不会在缓冲区满时放入数据,消费者也不会在缓冲区空时消耗数据。不够完善的解决方法容易出现死锁的情况,此时进程都在等待唤醒。
生产者消费者问题的java代码网上有很多,大佬们讲解的都非常详细,我这里就不过多赘述了。我这里使用的是wait()和notify()的方法,看起来这段代码很复杂,其实核心的东西只有几样,感觉java都把其中的具体细节原理全部封装好了,只需要调用就好了。这里面synchronized()函数的作用非常重要,给代码块或业务逻辑快添加锁(给代码块或业务逻辑快添加锁),它可以将produce和consum过程代码块同步起来,共享一个Storage资源。consumer和producer中的run是核心方法,也是控制打印的根源。
在Storage中,我还设置了几个属于类的变量,从而实现后面动画演示中参数需要。
package pcModel;
public class Producer implements Runnable {
private Storage storage;
public Producer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
/*
* if(PC.times==1) { storage.produce(); return; }else {
*/
do {
storage.produce();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} while (PC.stop == 0);
/* } */
}
}
package pcModel;
public class Consumer implements Runnable {
private Storage storage;
public Consumer(Storage storage) {
this.storage = storage;
}
@Override
public void run() {
/* if(PC.times==1) {
storage.consume();
return;
}else {*/
do {
storage.consume();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}while(PC.stop==0);
/*}*/
}
}
package pcModel;
import java.util.LinkedList;
import java.util.concurrent.LinkedBlockingQueue;
public class Storage {
public static int Scount=0;
public static int Pdoing=0;
public static int Cdoing=0;
public Storage(int count) {
this.MAX_SIZE = count;
this.Scount=0;
this.Pdoing=0;
this.Cdoing=0;
}
public Storage() {
this.Scount=0;
this.Pdoing=0;
this.Cdoing=0;
}
public static int num3;
// 仓库容量
private int MAX_SIZE = 1;
// 仓库存储的载体
private LinkedList<Object> list = new LinkedList<>();
public void produce() {
synchronized (list) {
while (list.size()+1> MAX_SIZE) {
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】仓库已满");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.add(new Object());
System.out.println("【生产者" + Thread.currentThread().getName()
+ "】生产一个产品,现库存" + list.size());
try {
Scount++;
Pdoing=1;
Cdoing=0;
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.notifyAll();
}
}
public void consume() {
synchronized (list) {
while (list.size() == 0) {
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】仓库为空");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove();
System.out.println("【消费者" + Thread.currentThread().getName()
+ "】消费一个产品,现库存" + list.size());
try {
Pdoing=0;
Cdoing=1;
Scount--;
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
list.notifyAll();
}
}
}
这里是输出重定向的类,具体原理查看参考文献,感觉很复杂,我就直接拿过来用了。
class JTextAreaOutputStream extends OutputStream {// 将控制台内容重定向到文本框中
private final JTextArea destination;
public JTextAreaOutputStream(JTextArea destination) {
if (destination == null)
throw new IllegalArgumentException("Destination is null");
this.destination = destination;
}
@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
final String text = new String(buffer, offset, length);
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
destination.append(text);
}
});
}
@Override
public void write(int b) throws IOException {
write(new byte[] { (byte) b }, 0, 1);
}
}
在这次项目的开发过程中,遇到了很多的困难。首先就是java和java swing编程的不同之处,需要将控制台内容显示再图形化界面上,经过多次摸索后,发现了将控制台输出内容重定向到控件上的方法;其次就是进程开始后无法停止,开启另一个线程后原先的线程还会继续执行,苦思冥想后无果,偶然和同学的讨论中发现可以声明全局变量来控制线程的停止,最终实现了停止功能;顺着全局变量的思想,考虑到可以用变量控制图片位置,进而实现动画模拟的效果,通过上网搜集相关函数,最终实现相关功能。
[1] Java多种方式解决生产者消费者问题(十分详细)https://blog.csdn.net/ldx19980108/article/details/81707751
[2] 图片的动态移动
https://www.cnblogs.com/fanfan-12511/p/6861223.html
[3] swing重定向输出到jtextArea
https://blog.csdn.net/A694543965/article/details/72083035
[4] https://download.csdn.net/download/grace_qiao/2589721
小白不易,大佬轻喷,欢迎大家批评指正。