just like I said in the title, I'm wondering how can I implement a counter in Java, where one thread counts up the variable and the other prints it out periodically? Obviously when I have two threads, I can't "transfer" the variable that I'm counting up to the other thread to print it out as the variable is only counted up in the scope of the while-loop.
I tried to create two threads, one that counts up the variable, the other one that prints it but the thread that periodically gives the value that's counted up prints 0 as the 0 in that scope is not changed
CodePudding user response:
I can't "transfer" the variable that I'm counting up to the other thread to print it out as the variable is only counted up in the scope of the while-loop.
.. but of course you can. You have a few approaches. The most obvious ones:
A shared field
You can have a field that two threads can access. Trivially:
class Test {
int x = 5;
void example() {
new Thread(this::run1).start();
new Thread(this::run2).start();
}
void run1() {
try {
Thread.sleep(1000L);
x = 5;
System.out.println("Incremented x!");
} catch (InterruptedException returnImmediately) {}
}
void run2() {
try {
Thread.sleep(1500L);
System.out.println("x is: " x);
} catch (InterruptedException returnImmediately) {}
}
}
However, careful! The above code is broken. It runs, but, the JVM reserves a few rights for itself. Specifically, a thread is allowed to reorder things and to use local caches for any and all variables (JVMs are allowed to do these things. That doesn't mean they must do so. Merely that they may. It can depend on how many CPU cores you have, what the OS is doing, and the phase of the moon. Any code that relies on the thread using a local cache is broken, as is any code that relies on the thread not doing this). The above code is therefore broken: Each thread may have its own x. A JVM that runs the above code is working according to specifications if this ends up printing '5'. It is also working according to specifications if it prints 10.
However, you can fix that problem: Various actions establish a 'happens before' relationship. The JVM guarantees that if you have 2 lines of code such that line A 'happens before' line B as per the list of official HA/HB establishing actions as per the JMM (Java Memory Model), then it is not possible to observe at B a state before A. (That's a fancy way to say: If A 'happens before' B, then it happens before B.. unless there is absolutely no way A directly affects B in any way in which case the JVM doesn't actually have to ensure this. But you'd never know - that's the point1).
So, if we can establish that x = 5;
has a happens-before relationship relative to System.out.println("x is: ")
we are home free. One trivial way to establish HB/HA is to declare a variable as volatile
, which ensures therte's some sort of HB/HA though it's hard to choose the direction with it. Given that these 2 threads have 500msec in between, volatile
would work fine here. But it's a bit rare volatile solves all your problems. A more universal approach is to lock on the same object (using the synchronized
keyword, or one of the lock types in the j.u.concurrent package). Another is to use one of the atomic classes. Let's do the latter:
class Test {
AtomicInteger x = new AtomicInteger(5);
void example() {
new Thread(this::run1).start();
new Thread(this::run2).start();
}
void run1() {
try {
Thread.sleep(1000L);
x.addAndGet(5);
System.out.println("Incremented x!");
} catch (InterruptedException returnImmediately) {}
}
void run2() {
try {
Thread.sleep(1500L);
System.out.println("x is: " x.get());
} catch (InterruptedException returnImmediately) {}
}
}
A message bus
Instead of communicating through shared fields, which is tricky and requires establishing HB/HA to do safely, but, is extremely hard to tesdt, given that a JVM does not have to make that fail, it may, you can also communicate to channels dedicated to the purpose of transmitting data from one concurrently running thing to another.
The most common tool for the job is a database. Databases work in transactions and have extensive support for ensuring communication occurs reliably and predictably. Generally this requires using the appropriate isolation level (Use SERIALIZABLE, and use a framework like JOOQ or JDBI to ensure retries are being handled. If you don't do that, you either [A] have a subtly broken app that WILL fail when your servers get busier, or [B] you better know what you are doing, because anything else is extremely finicky). Another option is a message bus system (where you can 'post' to a channel and 'subscribe' to one, a bit like a telegram / signal / whatsapp group, such as rabbitmq.
Writing an entire tutorial for that here is a bit excessive, but let's instead write a really rudimentary one by using one a concurrency-capable collection type. Again, from the java.util.concurrent
package. You should probably read the javadoc of all the stuff in that entire package straight t through, it's a very useful thing!
class Test {
var queue = new LinkedBlockingQueue<Integer>(100);
void example() {
new Thread(this::run1).start();
new Thread(this::run2).start();
}
void run1() {
try {
for (int i = 0; i < 1000; i ) {
// shove 'i' in the queue.
// if that is not possible because it is full, it'll wait.
queue.put(i);
System.out.println("added to queue: " i);
Thread.sleep(50L);
}
} catch (InterruptedException returnImmediately) {}
}
void run2() {
try {
while (true) {
// grab an item; waits if there isn't one right now.
int v = queue.take();
System.out.println("retrieved: " v);
if (v % 10 == 0) {
System.out.println("v was divisible by 10, let's wait a while");
Thread.sleep(200L);
}
}
} catch (InterruptedException returnImmediately) {}
}
}
[1] Almost.. the one thing the JMM and HB/HA relationships don't actually guarantee is timing. Hence, you can observe some pretty crazy things, but only if you derive information by checking how long things take. Showing code that does this is essentially academic, quite complicated, and well beyond the scope of this question.