I wanted a way to have multiple threads, but to have only one thread running at a time, controlling which thread ran when and how much code each thread would run at a time.
The solution I've arrived at is to have a controller thread and worker threads. The controller thread cedes control in round robin fashion to each worker thread in turn. The worker threads contain 'cede points' at which they return control back to the controller.
The code uses 'wait' and 'notify', which I know is something of a rat's nest. I think I've done a reasonable job of handling concurrency problems, and the code seems to work, but I'm wondering if there are any hidden problems waiting to bite me. I also thought I'd share this code because I've seen other questions suggesting that deterministic thread control is hard/impossible to do, but this solution looks like it could be useful for certain use cases.
Thanks.
EDIT3: Amended, working code at the end of this post
import java.util.Vector;
public class Controller extends Thread{
private Vector<Thread> threads = new Vector<>();
public void addThread(Thread thread){
System.out.println("Controller adding thread of "+thread);
threads.add(thread);
}
@Override
public void run(){
// Loop over the threads I manage, running each one in turn until
// it hands back control to me
try {
while(true){
for (int i=0; i<threads.size(); i++){
synchronized(threads.get(i)){
threads.get(i).notify(); // Wake up the worker thread
threads.get(i).wait(); // And request to be woken by the worker thread when it's done
}
Thread.sleep(1000);
}
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
public class WorkerThread extends Thread{
private Controller controller;
private boolean registered = false;
public WorkerThread(Controller controller){
this.controller = controller;
}
protected void cedeControl() throws InterruptedException{
synchronized(this){
if (!registered){
controller.addThread(this);
registered = true;
}
else {
this.notify(); // Give control back to the controller
}
this.wait(); // And request to be awoken again when the controller wants
}
}
}
public class ExampleThread extends WorkerThread{
public ExampleThread(Controller controller){
super(controller);
}
@Override
public void run(){
try{
while (true){
cedeControl();
System.out.println("Thread "+this+" running part 1");
cedeControl();
System.out.println("Thread "+this+" running part 2");
cedeControl();
System.out.println("Thread "+this+" running part 3");
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
public class Test{
public static void main(String[] args){
Test t = new Test();
t.run();
}
public void run(){
Controller controller = new Controller();
controller.start();
for (int i=0; i<3; i++){
Thread exampleThread = new ExampleThread(controller);
exampleThread.start();
}
}
}
Output:
Controller adding thread of Thread[Thread-1,5,main]
Controller adding thread of Thread[Thread-2,5,main]
Controller adding thread of Thread[Thread-3,5,main]
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-3,5,main] running part 1
Thread Thread[Thread-1,5,main] running part 2
Thread Thread[Thread-2,5,main] running part 2
Thread Thread[Thread-3,5,main] running part 2
Thread Thread[Thread-1,5,main] running part 3
Thread Thread[Thread-2,5,main] running part 3
Thread Thread[Thread-3,5,main] running part 3
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-3,5,main] running part 1
etc
EDIT: I've rewritten it to use a semaphore. Thanks to @GPI for the suggestion. It looks to work fine. I'm slightly concerned that the same thread could reacquire the semaphore if the other threads haven't yet tried to acquire it for some reason, but it's certainly a lot simpler than my wait/notify solution.
EDIT2: Hmm, as I feared the semaphore solution isn't robust. Looking through its output I found this:
Thread Thread[Thread-0,5,main] running part 3
Thread Thread[Thread-2,5,main] running part 2
Thread Thread[Thread-1,5,main] running part 3
Thread Thread[Thread-0,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 3
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-0,5,main] running part 2
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-1,5,main] running part 2
For reference, this is the semaphore code:
import java.util.concurrent.Semaphore;
public class ExampleThread extends Thread{
private Semaphore semaphore;
public ExampleThread(Semaphore semaphore){
this.semaphore = semaphore;
}
@Override
public void run(){
try{
while (true){
semaphore.acquire();
System.out.println("Thread "+this+" running part 1");
semaphore.release();
semaphore.acquire();
System.out.println("Thread "+this+" running part 2");
semaphore.release();
semaphore.acquire();
System.out.println("Thread "+this+" running part 3");
semaphore.release();
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
public class Test{
public static void main(String[] args){
Test t = new Test();
t.run();
}
public void run(){
Semaphore semaphore = new Semaphore(1, true); // The true makes it fair
for (int i=0; i<3; i++){
Thread exampleThread = new ExampleThread(semaphore);
exampleThread.start();
}
}
}
Output:
Thread Thread[Thread-0,5,main] running part 1
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-0,5,main] running part 2
Thread Thread[Thread-1,5,main] running part 2
Thread Thread[Thread-2,5,main] running part 2
Thread Thread[Thread-0,5,main] running part 3
Thread Thread[Thread-1,5,main] running part 3
Thread Thread[Thread-2,5,main] running part 3
Thread Thread[Thread-0,5,main] running part 1
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-0,5,main] running part 2
Thread Thread[Thread-1,5,main] running part 2
Thread Thread[Thread-2,5,main] running part 2
Thread Thread[Thread-0,5,main] running part 3
Thread Thread[Thread-1,5,main] running part 3
Thread Thread[Thread-2,5,main] running part 3
etc
EDIT3: I have what I consider to be a working version using wait and notify, with a registration method and guarding against spurious wakeups. Code is:
import java.util.Vector;
public class Controller extends Thread{
private Vector<Thread> threads = new Vector<>();
private volatile Thread currentThread; // Needed to cope with spurious wakeups
// see https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#wait-long-
public void addThread(Thread thread){
System.out.println("Controller adding thread of "+thread);
threads.add(thread);
}
public Thread getCurrentThread(){
return currentThread;
}
public void setCurrentThread(Thread currentThread){
this.currentThread=currentThread;
}
@Override
public void run(){
// Loop over the threads I manage, running each one in turn until
// it hands back control to me
try {
while(true){
for (int i=0; i<threads.size(); i++){
synchronized(threads.get(i)){
setCurrentThread(threads.get(i));
threads.get(i).notify(); // Wake up the worker thread
do {
threads.get(i).wait(); // And pause myself
} while (getCurrentThread() != this); // Spurious wakeup guard
// see https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#wait-long-
}
Thread.sleep(1000);
}
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
public class WorkerThread extends Thread{
private Controller controller;
private boolean registered = false;
public WorkerThread(Controller controller){
this.controller = controller;
}
protected void register() throws InterruptedException{
synchronized(this){
if (!registered){
controller.addThread(this);
registered = true;
do {
this.wait(); // Request to be woken when the controller calls on me
} while (controller.getCurrentThread() != this); // Spurious wakeup guard
// see https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#wait-long-
}
else {
throw new RuntimeException("Thread has already been registered!");
}
}
}
protected void cedeControl() throws InterruptedException{
synchronized(this){
if (!registered){
throw new RuntimeException("Thread has not been registered!");
}
else {
controller.setCurrentThread(controller);
this.notify(); // Give control back to the controller
do {
this.wait(); // And request to be awoken again when the controller wants
} while (controller.getCurrentThread() != this); // Spurious wakeup guard
// see https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#wait-long-
}
}
}
}
public class ExampleThread extends WorkerThread{
public ExampleThread(Controller controller){
super(controller);
}
@Override
public void run(){
try{
register(); // Put thread under the control of the controller
while (true){
System.out.println("Thread "+this+" running part 1");
cedeControl();
System.out.println("Thread "+this+" running part 2");
cedeControl();
System.out.println("Thread "+this+" running part 3");
cedeControl();
}
}
catch (Exception e){
e.printStackTrace();
}
}
}
public class Test{
public static void main(String[] args){
Test t = new Test();
t.run();
}
public void run(){
Controller controller = new Controller();
controller.start();
for (int i=0; i<3; i++){
Thread exampleThread = new ExampleThread(controller);
exampleThread.start();
}
}
}
Output:
Controller adding thread of Thread[Thread-1,5,main]
Controller adding thread of Thread[Thread-2,5,main]
Controller adding thread of Thread[Thread-3,5,main]
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-3,5,main] running part 1
Thread Thread[Thread-1,5,main] running part 2
Thread Thread[Thread-2,5,main] running part 2
Thread Thread[Thread-3,5,main] running part 2
Thread Thread[Thread-1,5,main] running part 3
Thread Thread[Thread-2,5,main] running part 3
Thread Thread[Thread-3,5,main] running part 3
Thread Thread[Thread-1,5,main] running part 1
Thread Thread[Thread-2,5,main] running part 1
Thread Thread[Thread-3,5,main] running part 1
etc