-
Notifications
You must be signed in to change notification settings - Fork 9
Interfaces, Threading and Synchronisation
This page serves to give you an overview of how Java deals with multi-threading (doing multiple things at the same time). This requires a knowledge of Java interfaces (hence their inclusion) as well as how to make sure these threads are kept under control and synchronised where necessary by use of Semaphore
objects.
These are vital concepts to understand if you are to create your own custom GUI.
In Java, you can define something called an "interface", which is much like a class except none of its methods have any code. These interfaces can then be "implemented" by other classes or can be implemented and instatiated directly.
To better understand this, let's consider the following example:
public interface Stringable {
public String toString();
}
The above interface, Stringable
, defines a method toString()
which returns a string. If we were then to create classes and say that they implement Stringable
:
public class DataRow implements Stringable { ... }
public class DataPoint implements Stringable { ... }
public class Bob implements Stringable { ... }
we are essentailly "advertising" to the rest of our program that DataRow
, DataPoint
and Bob
objects will all have a method called toString()
that returns a string. This lets us write code that takes a Stringable
object and uses the toString()
method without having to know what class it actually is. For example, we could write a method to write a line to a file that accepts anything that is Stringable
like so:
public void writeLine(Stringable toWrite) {
file.println(toWrite.toString());
}
That is to say, it doesn't matter if we pass writeLine(...)
a DataRow
, DataPoint
or Bob
object because all it cares about is whether it has a toString()
method.
As mentioned, you can implement and instatiate an interface directly too, without having to define a class, like so:
Stringable object = new Stringable() {
public String toString() {
return "It's a string!";
}
};
writeLine(object);
This is made even easier by the use of a so-called "lambda expression". If you have an interface that defines a single method (like our Stringable
example), you can use the following short-hand:
Stringable object = () -> {
return "It's a string!";
};
If your single interface method takes arguements like so:
public interface Stringable {
public String toString(int argumentOne, String argumentTwo);
}
then you simply put them in the ()
part of the expression:
Stringable object = (arg1, arg2) -> {
return String.format("Number: %d, String: %s", arg1, arg2);
};
Java provides several short-hand ways of writing out these. If, like the above example, your lambda expression contains only a return
line, then you can simplify it even further:
Stringable object = (arg1, arg2) -> String.format("Number: %d, String: %s", arg1, arg2);
Perhaps the most useful end to which interfaces are used is to provide a means of you effectively passing code as an argument. The purest form of this comes as the Runnable
interface:
public interface Runnable {
public void run();
}
Runnable
objects are generally used to define what code should run after something happens. For example, let's say you have created a device class and you want to be able to define some code that should run every time it is used to get a measurement from the device:
public class Device extends VISADevice {
...
private Runnable runOnMeasure = () -> {};
public void setOnMeasure(Runnable toRun) {
runOnMeasure = toRun;
}
public double getMeasurement() {
double value = queryDouble("MEASURE:VALUE?");
runOnMeasure.run();
return value;
}
}
This would let you do the following:
Device device = new Device(...);
device.setOnMeasure(() -> {
System.out.println("Measurement taken!");
});
which will effectively store the code given to the setOnMeasure
method for later and make it so that it is run every time getMeasurement()
is called. That is to say, now every time we make a measurement with this device, the line "Measurement taken!" will be output to the terminal.
You might recognise this sort of set-up from the SMU
class, specifically the sweep methods where you can define what should happen on each measurement during a sweep like so:
smu.doLinearSweep(
SMU.Source.CURRENT,
0,
50e-3,
10,
500,
true,
(count, point) -> {
System.out.printf("Measurement: %d; ", count);
System.out.printf("V = %e Volts; ", point.voltage);
System.out.printf("I = %e Amps\n", point.current);
}
);
causing lines like:
Measurement: 10; V = 25e-6 Volts; I = 50e-3 Amps
to be written to the terminal upon the completion of each measurement in the sweep.
There is another fairly useful interface that is often used. This is the so-called Predicate<...>
interface. As denoted by the <...>
this is a generic interface (ie it can be used with different types). It is often used to specify a filter or condition by providing a method that tests a given object and returns true or false based on some logic. The interface looks like this:
public interface Predicate<E> {
boolean test(E toTest);
}
An example of how this is used is when you want to filter a list of items based on a condition. Let's say we have an ArrayList
of String
objects. That is:
List<String> list = new ArrayList<>();
list.add("Hello");
list.add("Goodbye");
list.add("Alice");
list.add("Bob");
list.add("Eve");
So we basically have: ["Hello", "Goodbye", "Alice", "Bob", "Eve"]
. List
objects provide a means of removing all elements from themselves that match a given set of criteria. To do this, you provide a Predicate<>
to specify these criteria.
For example, if we wanted to remove all strings in our list that contain the letter "e", then we'd create the following predicate:
Predicate<String> filter = (toTest) -> {
return toTest.contains("e");
};
or, if you're old-school and want to write it out fully:
Predicate<String> filter = new Predicate<String>() {
boolean test(String toTest) {
return toTest.contains("e");
}
};
We can then give this Predicate
to the list telling it to remove any elements that return true when given to our Predicate
:
list.removeIf(filter);
The result will be that our list now only contains "Bob"
since everything else contains the letter "e".
We could, of course, do this all at once like so:
list.removeIf((v) -> {
return v.contains("e");
});
Since our expression only contains a return statement all on one line, we can actually shorten this further by ditching the {
, }
and return
like so:
list.removeIf((v) -> v.contains("e"));
What the List
effectively does is it loops through all of its elements, passing each to the test
method in our Predicate
and remove the element if it returns true
. We could do the same externally like so:
List<String> toRemove = new ArrayList<>();
for (String element : list) {
if (filter.test(element)) {
toRemove.add(element);
}
}
list.removeAll(toRemove);
Note that we build up a list of what we want to remove and then actually do the removal after the loop. This is necessary as you cannot modify a list whilst looping over it becuase doing so could potential alter the order of items in the list, thus meaning the loop would have to adjust (which is something it can't do).
If you really need to though, you will have to work in terms of the list's
Iterator
:
Iterator<String> itr = list.iterator();
// Keep going so-long as there is another item in the list
while (itr.hasNext()) {
// Retrieve the next item in the list
String element = itr.next();
if (filter.test(element)) {
itr.remove(); // Remove the last returned item
}
}
When making a simple command-line based program, it is easy to think that a program is just a sequence of steps executed one after the other. In truth, this need not be the case. Computers are perfectly capable of performing multiple tasks at once and so are individual programs. To do this, we introduce the concept of a "thread". Each thread is like a traditional program, executing one step after another, but they run concurrently to one another. Therefore, you can have one thread performing one task whilst another thread does something else.
In Java, multi-threading is made easy by use of Thread
objects. When creating a Thread
you give it a Runnable
object to be executed on this new thread when it is started. For example, if I wanted my program to add up all the integers from 0 to 1,000,000 whilst simultaneously calculating 20-factorial, you can create two threads like so:
Thread summingThread = new Thread(() -> {
int total = 0;
for (int i = 0; i <= 1000000; i++) {
total += i;
}
System.out.printf("Sum = %d\n", total);
});
Thread factorialThread = new Thread(() -> {
int total = 1;
for (int i = 1; i <= 20; i++) {
total *= i;
}
System.out.printf("20! = %d\n", total);
});
Then set them going:
summingThread.start();
factorialThread.start();
and they will go about executing the code defined in the Runnable
objects given to each of them quite happily and independently of the rest of your code (ie your program will continue after the start()
calls straight-away).
In the context of GUIs, this is quite important as you don't want the code that controls, draws and updates your GUI to be running on the same thread as your underlying processes. If it was, then whenever your program is busy doing something time-consuming (like downloading a file, or waiting for a device to respond to a read request), your GUI would freeze as your thread is completely consumed with the other process, making it look like your program has crashed to the user.
Sometimes, you will want separate threads to somehow be in-synch with each other. For example, you might want your main thread to wait until something is complete in another thread. This can be achieved by using a Semaphore
object:
Semaphore semaphore = new Semaphore(0);
Semaphores are like a ticketing system, they have a certain number of tickets or "permits" to give out (initially defined in the constructor, in our case our semaphore starts off with 0 permits). When a thread tries to acquire a permit from a semaphore but it has none left to give out, the thread will wait until it does, that is until another thread tells the semaphore to "release" a new permit. For example:
Semaphore semaphore = new Semaphore(0);
Thread thread = new Thread(() -> {
// ... does stuff ...
// Only continue when a permit is available
try {
semaphore.acquire();
} catch (InterruptedException e) { ... }
// ... does more stuff ...
});
thread.start();
System.out.println("The thread is running, press enter when you are ready");
// Wait for the user to hit enter
System.in.read();
// Release a permit
semaphore.release();
In the above example, a new thread is started which will run up to the point where it calls semaphore.acquire()
. As it stands, if nothing else happens the thread will wait at this point as our Semaphore
object has no permits to give (we created it with 0 permits). Meanwhile, the main thread has carried on and is waiting for the user to press the enter key in the terminal. After doing so, a new permit is given to the semamphore by use of semaphore.release()
at which point the other thread can then continue as semaphore.acquire()
will return since the Semaphore
now has a permit to give.
Any method that "blocks" a thread (ie makes it wait) can be "interrupted" by certain events (for example the program being ended early). When this happens, such methods like semaphore.acquire()
will throw an InterruptedException
, hence why we need the try {...} catch (...) {...}
structure around semaphore.acquire()
.
This is the basis of synchronising methods running on separate threads. Typically this is used to make sure one thread can't do something before another thread is ready to deal with the results of it. In the case of a GUI, you will likely use them to make sure your code doesn't go any further until a window or GUI element has been fully initialised.
- Getting Started
- Object Orientation
- Choosing a Language
- Using JISA in Java
- Using JISA in Python
- Using JISA in Kotlin
- Exceptions
- Functions as Objects
- Instrument Basics
- SMUs
- Thermometers (and old TCs)
- PID and Temperature Controllers
- Lock-Ins
- Power Supplies
- Pre-Amplifiers
- Writing New Drivers