Hi! Today we'll continue to consider the features of multithreaded programming and talk about thread synchronization.
What is synchronization in Java?
Outside of the programming domain, it implies an arrangement that allows two devices or programs to work together. For example, a smartphone and computer can be synchronized with a Google account, and a website account can be synced with social network accounts so you can use them to sign in. Thread synchronization has a similar meaning: it's an arrangement in which threads interact with each other. In previous lessons, our threads lived and worked separately from each other. One performed a calculation, a second slept, and a third displayed something on the console, but they didn't interact. In real programs, such situations are rare. Multiple threads can actively work with and modify the same data set. This creates problems. Imagine multiple threads writing text to the same place, for example, to a text file or the console. In this case, the file or console becomes a shared resource. The threads are unaware of each other's existence, so they simply write everything they can in the time allotted to them by the thread scheduler. In a recent lesson, we saw an example of where this leads. Let's recall it now: The reason lies in the fact that the threads are working with a shared resource (the console) without coordinating their actions with each other. If the thread scheduler allocats time to Thread-1, then it instantly writes everything to the console. What other threads have or have not already managed to write doesn't matter. The result, as you can see, is depressing. That's why they introduced a special concept, the mutex (mutual exclusion), to multithreaded programming. The purpose of a mutex is to provide a mechanism so that only one thread has access to an object at a certain time. If Thread-1 acquires object A's mutex, the other threads won't be able to access and modify the object. The other threads must wait until object A's mutex is released. Here's an example from life: imagine that you and 10 other strangers are participating in an exercise. Taking turns, you need to express your ideas and discuss something. But because you're seeing each other for the first time, in order to not constantly interrupt each other and fly into a rage, you use a 'talking ball': only the person with the ball can speak. This way you end up having a good and fruitful discussion. Essentially, the ball is a mutex. If an object's mutex is in the hands of one thread, other threads can't work with the object. You don't need to do anything to create a mutex: it's already built into theObject
class, which means that every object in Java has one.
How the synchronized operator works
Let's get to know a new keyword: synchronized. It is used to mark a certain block of code. If a code block is marked with thesynchronized
keyword, then that block can only be executed by one thread at a time.
Synchronization can be implemented in different ways. For example, by declaring an entire method to be synchronized:
public synchronized void doSomething() {
// ...Method logic
}
Or write a code block where synchronization is performed using some object:
public class Main {
private Object obj = new Object();
public void doSomething() {
// ...Some logic available simultaneously to all threads
synchronized (obj) {
// Logic available to just one thread at a time
}
}
}
The meaning is simple. If one thread goes inside the code block marked with the synchronized
keyword, it instantly captures the object's mutex, and all other threads trying to enter the same block or method are forced to wait until the previous thread completes its work and releases the monitor.
By the way! During the course, you've already seen examples of synchronized
, but they looked different:
public void swap()
{
synchronized (this)
{
// ...Method logic
}
}
The topic is new for you. And, of course, there will be confusion with the syntax. So, memorize it right away to avoid being confused later by the different ways of writing it.
These two ways of writing it mean the same thing:
public void swap() {
synchronized (this)
{
// ...Method logic
}
}
public synchronized void swap() {
}
}
In the first case, you're create a synchronized block of code immediately upon entering the method. It is synchronized by the this
object, i.e. the current object.
And in the second example, you apply the synchronized
keyword to the entire method. This makes it unnecessary to explicitly indicate the object being used for synchronization. Since the entire method is marked with the keyword, the method will automatically be synchronized for all instances of the class.
We won't dive into a discussion about which way is better. For now, choose whichever way you like best :) The main thing is to remember: you can declare a method synchronized only when all of its logic is executed by one thread at a time.
For example, it would be a mistake to make the following doSomething()
method synchronized:
public class Main {
private Object obj = new Object();
public void doSomething() {
// ...Some logic available simultaneously to all threads
synchronized (obj) {
// Logic available to just one thread at a time
}
}
}
As you can see, part of the method contains logic that does not require synchronization. That code can be run by multiple threads at the same time, and all critical places are set apart in a separate synchronized
block.
And one more thing. Let's closely examine our example from the lesson with name swapping:
public void swap()
{
synchronized (this)
{
// ...Method logic
}
}
Note: synchronization is performed using this
. That is, using a specific MyClass
object.
Suppose we have 2 threads (Thread-1
and Thread-2
) and only one MyClass myClass
object. In this case, if Thread-1
calls the myClass.swap()
method, the object's mutex will be busy, and when attempting to call the myClass.swap()
method Thread-2
will hang while waiting for the mutex to be released.
If we will have 2 threads and 2 MyClass
objects (myClass1
and myClass2
), our threads can easily simultaneously execute the synchronized methods on different objects.
The first thread executes this:
myClass1.swap();
The second executes this:
myClass2.swap();
In this case, the synchronized
keyword inside the swap()
method won't affect the operation of the program, since synchronization is performed using a specific object. And in the latter case, we have 2 objects. Thus, the threads don't create problems for each other. After all, two objects have 2 different mutexes, and acquiring one is independent of acquiring the other.
Special features of synchronization in static methods
But what if you need to synchronize a static method?
class MyClass {
private static String name1 = "Ally";
private static String name2 = "Lena";
public static synchronized void swap() {
String s = name1;
name1 = name2;
name2 = s;
}
}
It's not clear what role the mutex will play here.
After all, we already determined that each object has a mutex. But the problem is that we don't need objects to call the MyClass.swap()
method: the method is static! So what's next? :/
There's actually no problem here. Java's creators took care of everything :)
If a method that contains critical concurrent logic is static, then synchronization is performed at the class level.
For greater clarity, we can rewrite the above code as follows:
class MyClass {
private static String name1 = "Ally";
private static String name2 = "Lena";
public static void swap() {
synchronized (MyClass.class) {
String s = name1;
name1 = name2;
name2 = s;
}
}
}
In principle, you could have thought of this yourself: Because there are no objects, the synchronization mechanism must somehow be baked into the class itself. And that's the way it is: we can use classes to synchronize.
GO TO FULL VERSION