/ Java  

Java Multithreading 2: Synchronized Keyword

Principle of synchronized

In Java, each object has only one synchronization lock. This also means that the synchronization lock is dependent on the existance of object.

When we call the synchronized method of an object, we acquire the synchronization lock of the object. For example, synchronized(obj) acquires the synchronization lock of “obj”.

The access from different threads to synchronization locks are mutually exclusive. In other words, at a certain point in time, the synchronization lock of an object can only be acquired by one thread! With synchronization locks, we can achieve mutually exclusive access to “objects/methods” in multiple threads environment. For example, there are now two threads A and B, both of which will access the “synchronous lock of object obj”. Suppose, at a certain moment, thread A acquires “obj’s synchronization lock” and is performing some operations; and at this time, thread B also attempts to acquire “obj’s synchronization lock” - thread B will fail to acquire it and must wait, Until thread A releases the “synchronization lock of the obj”, thread B cannot acquire the “synchronization lock of obj”.

Basic rules of synchronized

When a thread accesses the “synchronized method” or “synchronized code block” of “some object”, other threads’ access to the “synchronized method” or “synchronized code block” of “this object” will be blocked

Example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class MyRunable implements Runnable {
@Override
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // sleep 100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}

public class Demo1_1 {
public static void main(String[] args) {
Runnable demo = new MyRunable(); // create “Runnable”

Thread t1 = new Thread(demo, "t1"); // create “thread t1”
Thread t2 = new Thread(demo, "t2"); // create “thread t2”
t1.start(); // start “thread t1”
t2.start(); // start “thread t2”
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t1 loop 0
t1 loop 1
t1 loop 2
t1 loop 3
t1 loop 4
t2 loop 0
t2 loop 1
t2 loop 2
t2 loop 3
t2 loop 4

The synchronized (this) exists in the run() method, and both t1 and t2 are threads created based on demo. We can think of this in synchronized(this) as the demo. Threads t1 and t2 share the “demo synchronization lock”. Therefore, when one thread is running, the other thread must wait for the “running thread” to release the “demo synchronization lock” before running.

Then let’s modify the above code, and then run to see how is the results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyThread extends Thread {

public MyThread(String name) {
super(name);
}

@Override
public void run() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // sleep 100ms
System.out.println(Thread.currentThread().getName() + " loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}

public class Demo1_2 {
public static void main(String[] args) {
Thread t1 = new Thread(demo, "t1"); // create “thread t1”
Thread t2 = new Thread(demo, "t2"); // create “thread t2”
t1.start(); // start “thread t1”
t2.start(); // start “thread t2”
}
}

This time MyThread class directly inherits from Thread, and both t1 and t2 are MyThread child threads.

Results:

1
2
3
4
5
6
7
8
9
10
t1 loop 0
t2 loop 0
t1 loop 1
t2 loop 1
t1 loop 2
t2 loop 2
t1 loop 3
t2 loop 3
t1 loop 4
t2 loop 4

This in synchronized(this) refers to the “current class object”, that is, the current object corresponding to the class where synchronized(this) is located. Its role is to acquire the “synchronization lock of the current object”.

For Demo1_2, this in synchronized(this) represents the MyThread object, and t1 and t2 are two different MyThread objects, so when t1 and t2 execute synchronized(this), they acquire synchronization locks of different objects. For Demo1_1, this in synchronized(this) represents the MyRunable object; t1 and t2 share a MyRunable object. Therefore, one thread acquires the synchronization lock of the object and causes another thread to wait.

When a thread accesses the “synchronized method” or “synchronized code block” of “some object”, other threads can still access the “non synchronized code block” of “this object”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Count {
// method with synchronized block
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // sleep 100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}

// non synchronized block
public void nonSynMethod() {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}

public class Demo2 {
public static void main(String[] args) {
final Count count = new Count();

// create t1, t1 will call count object's synMethod()
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod();
}
}, "t1");

// create t2, t2 will call count object's nonSynMethod()
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.nonSynMethod();
}
}, "t2");


t1.start(); // start t1
t2.start(); // start t2
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t1 synMethod loop 0
t2 nonSynMethod loop 0
t1 synMethod loop 1
t2 nonSynMethod loop 1
t1 synMethod loop 2
t2 nonSynMethod loop 2
t1 synMethod loop 3
t2 nonSynMethod loop 3
t1 synMethod loop 4
t2 nonSynMethod loop 4

Main thread creates t1 and t2. t1 will call the synMethod() method of the count object, which contains the synchronization block; and t2 will call the nonSynMethod() method of the count object, which is not a synchronization method. While t1 is running, although synchronized(this) is called to obtain the count synchronization lock. It does not cause t2 to block because t2 does not use the “count synchronization lock.

When a thread accesses the “synchronized method” or “synchronized code block” of “an object”, other threads’ access to other “synchronized methods” or “synchronized code blocks” of the “object” will be blocked.

We modify the nonSynMethod() method in the above example with synchronized (this)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Count {
// method with synchronized block
public void synMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // sleep 100ms
System.out.println(Thread.currentThread().getName() + " synMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}

// synchronized block 2
public void nonSynMethod() {
synchronized(this) {
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + " nonSynMethod loop " + i);
}
} catch (InterruptedException ie) {
}
}
}
}

public class Demo2 {
public static void main(String[] args) {
final Count count = new Count();

// create t1, t1 will call count object's synMethod()
Thread t1 = new Thread(
new Runnable() {
@Override
public void run() {
count.synMethod();
}
}, "t1");

// create t2, t2 will call count object's nonSynMethod()
Thread t2 = new Thread(
new Runnable() {
@Override
public void run() {
count.nonSynMethod();
}
}, "t2");


t1.start(); // start t1
t2.start(); // start t2
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t1 synMethod loop 0
t1 synMethod loop 1
t1 synMethod loop 2
t1 synMethod loop 3
t1 synMethod loop 4
t2 nonSynMethod loop 0
t2 nonSynMethod loop 1
t2 nonSynMethod loop 2
t2 nonSynMethod loop 3
t2 nonSynMethod loop 4

Main thread creates t1 and t2. synchronized(this) is called when both t1 and t2 run, this is the count object, and t1 and t2 share the count. Therefore, when t1 is running, t2 will be blocked and wait for t1 to run and release the count synchronization lock.

Synchronized method and Synchronized block

Synchronized method is to modify the method with synchronized

1
2
3
public synchronized void foo1() {
System.out.println("synchronized methoed");
}

Synchronized code block is to modify the code block with synchronized.

1
2
3
4
5
public void foo2() {
synchronized (this) {
System.out.println("synchronized methoed");
}
}

this in the synchronized code block refers to the current object. You can also replace this with other objects, for example, replace this with obj, then foo2() acquires the synchronization lock of obj when executing synchronized(obj).

Synchronized code blocks can more precisely control conflicts and restrict access to areas, and sometimes perform more efficiently.

Instance lock and global lock

Instance loc

  • lock on an instance object. If the class is a singleton, then the lock also has the concept of a global lock.
  • The instance lock corresponds to the synchronized keyword.

Global lock

  • The lock is for a class, no matter how many objects are instantiated, the thread shares the lock.
  • The global lock corresponds to static synchronized (or locked on the class or classloader object of the class)

Let’s look at the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Something {
public synchronized void isSyncA(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // Sleep 100ms
System.out.println(Thread.currentThread().getName()+" : isSyncA");
}
}catch (InterruptedException ie) {
}
}

public synchronized void isSyncB(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // Sleep 100ms
System.out.println(Thread.currentThread().getName()+" : isSyncB");
}
}catch (InterruptedException ie) {
}
}

public static synchronized void cSyncA(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // Sleep 100ms
System.out.println(Thread.currentThread().getName()+" : cSyncA");
}
}catch (InterruptedException ie) {
}
}

public static synchronized void cSyncB(){
try {
for (int i = 0; i < 5; i++) {
Thread.sleep(100); // Sleep 100ms
System.out.println(Thread.currentThread().getName()+" : cSyncB");
}
}catch (InterruptedException ie) {
}
}
}

Suppose, Something has two instances x and y. Analyze the locks acquired by the following 4 scenarios.

  1. x.isSyncA() and x.isSyncB()
  2. x.isSyncA() and y.isSyncA()
  3. x.cSyncA() and y.cSyncB()
  4. x.isSyncA() and Something.cSyncA()

x.isSyncA() and x.isSyncB()

Cannot be accessed simultaneously. Because isSyncA() and isSyncB() share the same synchronization locks on the object instance x!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class LockTest1 {
Something x = new Something();

// x.isSyncA() and x.isSyncB()
private void test1() {
// create t11, t11 will call x.isSyncA()
Thread t11 = new Thread(
new Runnable() {
@Override
public void run() {
x.isSyncA();
}
}, "t11");

// create t12, t12 will call x.isSyncA()
Thread t12 = new Thread(
new Runnable() {
@Override
public void run() {
x.isSyncB();
}
}, "t12");


t11.start(); // start t11
t12.start(); // start t12
}

public static void main(String[] args) {
LockTest1 demo = new LockTest1();
demo.test1();
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t11 : isSyncA
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB
t12 : isSyncB

x.isSyncA() and y.isSyncA()

Can be accessed simultaneously. Because t21 and t22 don’t try to acquire the same lock. x.isSyncA() accesses the synchronization lock of x, and y.isSyncA() accesses the synchronization lock of y.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LockTest2 {
Something x = new Something();
Something y = new Something();

// x.isSyncA() and y.isSyncA()
private void test2() {
// create t21, t22 will call x.isSyncA()
Thread t21 = new Thread(
new Runnable() {
@Override
public void run() {
x.isSyncA();
}
}, "t21");

// create t22, t22 will call x.isSyncA()
Thread t22 = new Thread(
new Runnable() {
@Override
public void run() {
y.isSyncA();
}
}, "t22");


t21.start(); // start t21
t22.start(); // start t22
}

public static void main(String[] args) {
LockTest2 demo = new LockTest2();
demo.test2();
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t21 : isSyncA
t22 : isSyncA
t21 : isSyncA
t22 : isSyncA
t21 : isSyncA
t22 : isSyncA
t22 : isSyncA
t21 : isSyncA
t22 : isSyncA
t21 : isSyncA

x.cSyncA() and y.cSyncB()

Cannot be accessed simultaneously. Because cSyncA() and cSyncB() are static, x.cSyncA() is equivalent to Something.isSyncA(), and y.cSyncB() is equivalent to Something.isSyncB(), so they share the same synchronization lock and cannot be acquired at the same time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class LockTest3 {
Something x = new Something();
Something y = new Something();

// x.cSyncA() and y.cSyncB()
private void test3() {
// create t31, t31 will call x.cSyncA()
Thread t31 = new Thread(
new Runnable() {
@Override
public void run() {
x.cSyncA();
}
}, "t31");

// create t32, t32 will call y.cSyncB()
Thread t32 = new Thread(
new Runnable() {
@Override
public void run() {
y.cSyncB();
}
}, "t32");


t31.start(); // start t31
t32.start(); // start t32
}

public static void main(String[] args) {
LockTest3 demo = new LockTest3();
demo.test3();
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t31 : cSyncA
t31 : cSyncA
t31 : cSyncA
t31 : cSyncA
t31 : cSyncA
t32 : cSyncB
t32 : cSyncB
t32 : cSyncB
t32 : cSyncB
t32 : cSyncB

x.isSyncA() and Something.cSyncA()

Can be accessed simultaneously. Because isSyncA() is an instance method, x.isSyncA() uses the lock of object x; while cSyncA() is a static method, Something.cSyncA() uses class lock. Therefore, they can be accessed simultaneously.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class LockTest4 {
Something x = new Something();

// x.isSyncA() and x.cSyncA()
private void test4() {
// create t41, t41 will call x.isSyncA()
Thread t41 = new Thread(
new Runnable() {
@Override
public void run() {
x.isSyncA();
}
}, "t41");

// create t42, t42 will call x.cSyncA()
Thread t42 = new Thread(
new Runnable() {
@Override
public void run() {
x.cSyncA();
}
}, "t42");


t41.start(); // start t41
t42.start(); // start t42
}

public static void main(String[] args) {
LockTest4 demo = new LockTest4();
demo.test4();
}
}

Results:

1
2
3
4
5
6
7
8
9
10
t41 : isSyncA
t42 : cSyncA
t42 : cSyncA
t41 : isSyncA
t42 : cSyncA
t41 : isSyncA
t42 : cSyncA
t41 : isSyncA
t41 : isSyncA
t42 : cSyncA