In some cases, we need to share objects among different threads. Generally, we must design a concurrency mechanism to handle different states of the shared objects. If the state of a shared object is updated by one thread, other threads need to be notified so they can handle the object accordingly.
Sometimes, we want to put restrictions on shared objects. For example, only read-only objects can be shared or there is only one instance of a class. This article introduces two concurrency patterns that help us to achieve the above goal.
Once an immutable object is created, its state (including its data) cannot be changed. So we can safely share it among threads.
A well known immutable class in Java is string. Once we try to modify a string object through methods like replace, a new copy will be generated and returned while the original object remains unchanged.
Typically, we can define an immutable class by declaring all of its fields as private, as well as disabling all setter methods.
The collections class in Java provides some useful methods to create immutable objects, for example unmodifiableList method returns an immutable view of a given list. Actually, the collections class generates a wrapper of the given list, with all setter methods disabled. If you try to call a setter method on the returned immutable view, an UnsupportedOperationException will be raised.
The following code snippet is from the source code of collections class. The UnmodifiableList class is a wrapper of a list. All modification operations, including sort method, are disabled to make sure this list is immutable.
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
static class UnmodifiableList < E > extends UnmodifiableCollection < E >
implements List < E > {
private static final long serialVersionUID = -283967356065247728 L;
final List < ? extends E > list;
UnmodifiableList(List < ? extends E > list) {
super(list);
this.list = list;
}
public boolean equals(Object o) {
return o == this || list.equals(o);
}
public int hashCode() {
return list.hashCode();
}
public E get(int index) {
return list.get(index);
}
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
public int indexOf(Object o) {
return list.indexOf(o);
}
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
public boolean addAll(int index, Collection < ? extends E > c) {
throw new UnsupportedOperationException();
}
@Override
public void replaceAll(UnaryOperator < E > operator) {
throw new UnsupportedOperationException();
}
@Override
public void sort(Comparator < ? super E > c) {
throw new UnsupportedOperationException();
}
}
Double-checked locking pattern tests the locking criterion before acquiring a lock. In this way, we can avoid unnecessary operations.
This pattern is typically used in multi-threaded version of singleton pattern. The singleton pattern aims at ensuring that a class has only one instance, also it provides a global static point of access.
The following class diagram shows the structure of singleton pattern.
The following code shows an example of single-thread singleton. The static method getInstance only creates an instance when the instance variable is null.
1 2 3 4 5 6 7 8 9 10 11//single thread version class Singleton { private Singleton instance; public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
In multi-threaded environment, if two threads call the getInstance method at the same time, they may both find the instance is not created. So two instances may be created.
To make the above singleton class thread-safe, we can use the “synchronized” keyword in Java.
1
2
3
4
5
6
7
8
9
10
11
// Correct but possibly expensive multiple threaded version
class Singleton {
private Singleton instance;
public static synchronized Singleton getInstance() {
if (instance == null) {
Instance = new Singleton();
}
return instance;
}
}
In the above implementation, if the instance has been created, we actually do not need to synchronize the getInstance method since we can return the same instance to concurrent calls from different threads.
Imagine there are two threads calling the getInstance method in the above example at the same time, only one thread can retrieve the instance even if the instance has been created. This is not efficient.
To solve this problem, we can create the instance only when it is needed. If the instance exists, just return it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19// Double checked locking version class Singleton { private Singleton instance; public static Singleton getInstance() { if (instance == null) { Synchronized(Singleton.class) { /* * if another thread acquired the lock before, * the instance may have been created */ if (instance == null) { Instance = new Singleton(); } } } return instance; } }
In the above code example, we use a second null checked because another thread acquired the lock before the instance may have been created.