简体   繁体   中英

Starting and Stopping Threads with Buttons

I have what I thought was a simple problem, which I have yet to find a good solution to: I would like to be able to pause and unpause the activity taking place in a thread, by hitting a button on a Swing interface panel.

Specifically, I would like to use one thread to take in audio frames in real time; a second thread to perform magic processing on those frames; and a third thread to serialize the results and send over a socket somewhere else. The kicker is that depending on the brand of magic we employ, the processing in the second thread might take longer per frame to perform than the actual data collection, and the data might pile up after a while.

As a very crude prototype workaround we thought we'd add a GUI with a button to turn the audio collection process on and off and a status bar (to be implemented later) so that a user could sort of keep an eye on how full the buffer (a linked blocking queue) happened to be.

This is harder than I anticipated. I've stripped the problem down to a toy version: A linked blocking queue that can store 50 Integers, a GUI, two threads (adding to and removing from the queue at different rates) and a Token object wrapped around a boolean. It looks like this, and it sorta works:

Test.java

public class Test {
  public static void main(String[] args) throws IOException {

    Token t1 = new Token();
    Token t2 = new Token();
    LinkedBlockingQueue<Integer> lbq = new LinkedBlockingQueue<Integer>(50);

    startFill sf = new startFill(t1, lbq);
    startEmpty se = new startEmpty(t2, lbq);

    TestUI testUI = new TestUI(t1, t2, lbq);
    testUI.setVisible(true);

    sf.start();
    se.start();
  }
}

TestUI.java

public class TestUI extends JFrame implements ActionListener {
  private JToggleButton fillStatus, emptyStatus;
  public boolean filling, emptying;
  public Token t1, t2;
  public LinkedBlockingQueue<Integer> lbq;

  public TestUI(Token t1, Token t2, LinkedBlockingQueue<Integer> lbq) {
    this.t1 = t1;
    this.t2 = t2;
    this.lbq = lbq;
    initUI();
  }

  public synchronized void initUI() {
    JPanel panel = new JPanel();
    panel.setLayout(null);

    filling = false;
    fillStatus = new JToggleButton("Not Filling");
    fillStatus.setBounds(20, 20, 150, 25);
    fillStatus.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent event) {
        if (filling == false) {
          fillStatus.setText("Filling");
        } else {
          fillStatus.setText("Not Filling");
        }
        filling = !filling;
        t1.flip();
        System.out.println("fill button press");
      }
    });

// Similar code for actionListener on Empty button, omitted

    panel.add(fillStatus);
    panel.add(emptyStatus);
    add(panel);
    setTitle("Test interface");
    setSize(420, 300);
    setLocationByPlatform(true);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
  }

  public void actionPerformed(ActionEvent e) {
  }
}

startFill.java

public class startFill extends Thread {
  public Token token;
  public LinkedBlockingQueue<Integer> lbq;

  public startFill(Token token, LinkedBlockingQueue<Integer> lbq) {
    this.token = token;
    this.lbq = lbq;
  }

  public void run() {
    int count = 0;
    while (true) {
      while (!token.running()) {
        try {
          sleep(200);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }

      while (token.running()) {
        try {
          lbq.put(count);
          System.out.println("queue size = " + lbq.size());
          count++;
          sleep(100);
        } catch (InterruptedException e1) {
          e1.printStackTrace();
        }
      }
    }
  }
}

There is also a startEmpty.java that works about the same way, and a Token.java that's a wrapper for a boolean state variable, omitted for merciful brevity.

So that works, but at the expense of polling in the while (!token.running()) loop. I tried using Locks and Conditions, but failed, always getting IllegalMonitorStateExceptions.

And I looked at this similar question and managed to get that working, but at the expense of using the yield() method which apparently differs significantly between Java 5 and Java 6, and seems to be highly discouraged.

So my question: Is there some correct, or significantly better way to do what I am trying to do? It seems like there should be a way to make this happen without the polling and with reliable methods.

Update: I'm not sure I can get around the issue of controlling the audio capture loop in some way for the application. Whether it is a human pressing a button, or internal logic making decisions based on some other factors, we really need to be able to shut the darn thing down and bring it back to life on command.

Instead of handling the synchronisation between your 3 worker processes by hand via a GUI, you could also setup a factory lineup between the workers:

  • add 2 queues between your workers
  • block your threads on queue-state conditions;
    • readers (consumers) block on empty queue
    • writers (producers) block when the queue is full (say 2n messages where n is the number of consumers for that queue.)
  • wait() on a queue to block your thread and notifyAll() on that queue after adding or removing a message from a queue.

A setup like this automatically slows down producers running faster than their consumers.

Why dont you implement ArrayBlockingQueue.

Its Better use ArrayBlockingQueue class which is present in java.util.concurrent package, which is Thread Safe.

BlockingQueue<String> queue = new ArrayBlockingQueue<String>(100);

Here is one way to do what I was trying to do: Properly use wait() and notify(), synchronized on the Token objects, like such:

startFill.java run() method

  public synchronized void run() {
    int count = 0;
    try {
      // token initializes false
      // wait until notification on button press
      synchronized (token) {
        token.wait();
      }

      // outer loop
      while (true) {
        // inner loop runs as long as token value is true
        // will change to false on button press
        while (token.running()) {
          lbq.put(count);
          System.out.println("queue size = " + lbq.size());
          count++;
          sleep(100);
        }

        // wait until notification on button press, again      
        synchronized (token) {
          token.wait();
        }
      }
    } catch (InterruptedException e2) {
      e2.printStackTrace();
    }
  }

TestUI.java ActionListener:

fillStatus.addActionListener(new ActionListener() {
  // t1 was initialized false
  public void actionPerformed(ActionEvent event) {
    if (filling == false) {
      fillStatus.setText("Filling");
      // if false, change t1 status to true
      t1.flip();
      // and send the notification to the startFill thread that it has changed
      synchronized (t1) {
        t1.notify();
      }
    } else {
      fillStatus.setText("Not Filling");
      // if true, change t1 status to false
      t1.flip();
      // no notification required due to polling nature of startFill's active thread
    }
    filling = !filling;
    System.out.println("fill button press");
  }
});

This works rather nicely, without polling while the thread is turned off.

My initial attempts at this failed due to bad syntax-- I neglected the synchronized (token) {...} context block around the wait() and notify() statements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM