Previous article : Java Thread Programming: Lesson 1
Threads are easy and fun when we don’t have to share data. However, when multiple threads want to get hold of shared resources, things get a little messy.
In our previous article, we have seen a server being handed a thread to deal with a client. Let’s say we want to count how many clients the server was able to serve successfully? How are we going to do that? One easy way is, we can have a counter variable and increment it whenever a job is done successfully.
package com.bazlur.threads;
public class Counter {
private long count;
public Counter(long count) {
this.count = count;
}
public void increment() {
count++;
}
public long getCount() {
return count;
}
}
We will pass this Counter
to each client handler, and when they are successful, the handler will increment it.
package com.bazlur.threads;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Playground {
public static void main(String[] args) throws IOException {
var serverSocket = new ServerSocket(4040);
System.out.println("Started server on port " + 4040);
Counter counter = new Counter(0);
while (true) {
var socket = serverSocket.accept();
var thread = new Thread(() -> {
handleClient(socket, counter);
});
thread.start();
}
}
private static void handleClient(Socket socket, Counter counter) {
//here we have code for handling a client
counter.increment();
}
}
So we have passed the counter reference to the handleClient()
method, and it will increment it when the job is done and successful.
However, this might cause some issues. For example, when multiple threads try to increment on a count
variable, some can end up incrementing simultaneously.
What happens is, when a thread tries to change a value, it first read it and then changes it. Let’s assume at a point in time, thread A
reads the value 32
and at the same time, another thread B
reads the same value, which is 32
. Now both will try to increment it, and they both ended up incrementing it to 33. In reality, it should be 34.
If this happens, one of the values will be written, and the other one would get lost, and we will end up with stale data. So let’s experiment with this hypothesis.
package com.bazlur.threads;
public class Playground {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter(0);
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1_000; i++) {
counter.increment();
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1_000; i++) {
counter.increment();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
var count = counter.getCount();
System.out.println("count = " + count);
}
}
In the above code, we have created an instance of Counter
and then create two threads.
Each will run 1000
times and increment the Counter. So if both threads have done their job, we will have 2000 as a value in the Counter
.
In java, we start our program in the main method, and the main method runs under a thread called the main
thread. Here, we have created two other threads from the main thread, and they are going to run separately.
When a thread finishes it’s work, it dies. Here, the main method can even finish its work before the other thread ends its work. Let’s look at the main method; it creates two threads, then starts those threads, finally invokes the getCount()
method to get the count and print it. While the two threads are incrementing, if the main thread finishes its work, then we will never get the final count. Therefore, the main thread has to wait for the other two threads to complete their work. To do that, we call the join() method of the reference of the thread.
That’s why in the above code, we have used the following two lines-
thread1.join();
thread2.join();
Now the main thread will wait until these threads finish. Only after that, the main thread will print the count value.
Now let’s run the above code. According to our calculation, the value should be 2000
.
However, it doesn’t matter how many times you run it; it will hardly be 2000
. Every time it will print different results.
You could be a little puzzled at this point, but this is what it is supposed to be. The reason is, the two threads increment the Counter
simultaneously, and it could happen that two threads increment the value at the same time. In such a case, one will win, and the other will be discarded, and we will end up with stale data. This is call contention. Whenever a shared variable is changed from multiple threads, this sort of situation emerges.
This is called a race condition.
We can avoid this if we could only allow one thread at a point in time when we want to change a value. We can do this by adding a lock to it.
Let’s change the counter class and add a look at its method -
package com.bazlur.threads;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private long count;
private Lock lock = new ReentrantLock();
public Counter(long count) {
this.count = count;
}
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
public long getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}
Now, if we run the Playground.java
, we will always get the correct result.
From this discussion, the conclusion that we can draw is, whenever data is shared into multiple threads, and they are changed, we have to put a locking; otherwise, they will produce stale data.
In the previous article, we have seen how we can improve our code using the execute-around pattern. We can use the same pattern in the Counter class and clean some boilerplate code.
class ConcurrentCounter {
private long count;
private final Lock lock = new ReentrantLock();
public long incrementAndGet() {
return withLock(lock, () -> ++count);
}
public long get() {
return withLock(lock, () -> count);
}
}
That’s for today, cheers!!