Reputation: 161
I just started learning "multithreading" in JAVA and it seams I don't understand how keyword "synchronized" works. I had four examples and they're very alike (in my opinion), and I don't understand why I get every time different results.
public class BufferThread1 {
static int counter = 0;
static StringBuffer s = new StringBuffer();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread1.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread1.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
while (BufferThread1.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}
}
Result:
run:
> 1 A
< 2 > 3 AA
AAB
< 5 AABB
< 6 AABBB
public class BufferThread2 {
static int counter = 0;
static StringBuilder s = new StringBuilder();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread2.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread2.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
while (BufferThread2.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}
}
Result:
run:
> 1 A
< 2 AB
< 3 ABB
< 4 ABBB
< 5 ABBBB
< 6 ABBBBB
public class BufferThread3 {
static int counter = 0;
static StringBuffer s = new StringBuffer();
public static void main(String args[]) throws InterruptedException {
new Thread() {
public void run() {
synchronized (s) {
while (BufferThread3.counter++ < 3) {
s.append("A");
System.out.print("> " + counter + " ");
System.out.println(s);
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
Logger.getLogger(BufferThread3.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
}.start();
Thread.sleep(100);
synchronized (s) {
while (BufferThread3.counter++ < 6) {
System.out.print("< " + counter + " ");
s.append("B");
System.out.println(s);
}
}}
}
Result:
run:
> 1 A
> 2 AA
> 3 AAA
< 5 AAAB
< 6 AAABB
Of course, I skipped import java.util.logging.Level; import java.util.logging.Logger;
I just don't realise how these examples work and synchronized here ! I do hope that someone help me.
Upvotes: 0
Views: 87
Reputation: 770
First example shown by Mr. Lawrey.
In the second example the "Background" thread only gets to do one print of and since StringBuilder is used this time instead of StringBuffer the main thread will not block while trying to print "s", hence only 1 A.
In the third example the main thread is blocked until the background thread terminates because you start the background thread before the main thread's loop. Thus the background thread will get 3 loops done and hence the 3 A's.
Though I suspect these are artificial examples for learning purposes it should still be noted that sleeping inside a synchronized block is NOT a good idea as this will not release the lock.
Upvotes: 0
Reputation: 7032
Let's try a simpler example that gets at what synchronized
does without such an overly complicated class.
Consider the following Runnable
task:
public class Task implements Runnable {
private final int id;
public Task(int id) {
this.id = id;
}
public void run() {
for(int i = 0; i < 5; i++) {
System.out.println("Task " + id + " prints " + i);
}
}
}
Then let's try running it with this main method:
public static void main(String[] args) {
Thread t1 = new Thread(new Task(1));
Thread t2 = new Thread(new Task(2));
t1.start();
t2.start();
}
The output could look something like this:
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
But it could also look like this:
Task 1 prints 0
Task 2 prints 0
Task 2 prints 1
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
Task 2 prints 4
The truth is, you have no guarantees about the order in which the two Tasks execute commands (with respect to each other. You still do know that each task will print 0...4
in order). The fact that t1.start()
comes before t2.start()
means nothing.
The synchronized
command allows for some control over which thread executes when. Essentially, the command synchronized(obj) {....}
means that at most one thread is allowed to execute the commands in the block (within the {...}
) at a time. This is know as mutual exclusion.
A nice way to think about it is as a locked room with a single key hanging outside on the wall. In order to get into the room, you have to take the key off the wall, unlock the door, go into the room, and lock it from the inside. Once you are done doing whatever you are doing in the room, you unlock the door from the inside, go outside, lock the door from the outside, and hang the key up. It is clear that while you are in the room no one else can join you, as the door is locked and you currently hold the only key to get in.
To illustrate, consider the following improved task class:
public class SynchronizedTask implements Runnable {
private final Object lock;
private final int id;
public Task(int id, Object lock) {
this.id = id;
this.lock = lock;
}
public void run() {
synchronized(lock) {
for(int i = 0; i < 5; i++) {
System.out.println("Task " + id + " prints " + i);
}
}
}
}
And run it with the following modified main method:
public static void main(String[] args) {
Object lock = new Object();
Thread t1 = new Thread(new Task(1, lock));
Thread t2 = new Thread(new Task(2, lock));
t1.start();
t2.start();
}
We still don't know whether t1
or t2
will enter the synchronized
block first. However, once has entered, the other must wait for it to finish. Thus, there are exactly two possible outputs of this code. Either t1
gets in first:
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
Or t2
gets in first:
Task 2 prints 0
Task 2 prints 1
Task 2 prints 2
Task 2 prints 3
Task 2 prints 4
Task 1 prints 0
Task 1 prints 1
Task 1 prints 2
Task 1 prints 3
Task 1 prints 4
It is important to note that this behavior only worked as desired because we used the same lock
object for both Tasks. If we had run the following code instead:
public static void main(String[] args) {
Thread t1 = new Thread(new Task(1, new Object()));
Thread t2 = new Thread(new Task(2, new Object()));
t1.start();
t2.start();
}
We would have identical behavior to the original un-synchronized code. This is because we now have a room with two (or really, infinite if we replicate our thread initialization) keys hanging outside. Thus while t1
is inside the synchronized
block, t2
can just use its own key to get in, defeating the whole purpose.
Upvotes: 1
Reputation: 533530
Your <
and >
label which thread is running here.
> 1 A
Your background thread is running only and prints this line as expected.
< 2
The main thread prints the counter 2
but cannot acquire the lock on s
so it blocks.
> 3 AA
The background thread increments the counter again and prints 3
and a second A. On the next iteration it exits and counter == 4
As the thread exits, it releases the lock.
AAB
The main thread can now acquire the lock and append("B")
< 5 AABB
The main thread increments the counter to 5 and adds another B
StringBuffer
is a pet hate of mine which was replaced more thna ten years ago by StringBuidler. It is almost impossible to implement a useful thread safe class using it and when people have tried, it has meant the class wasn't really thread safe. Note: SimpleDateFormat uses StringBuffer and it is not thread safe.
Upvotes: 1