How should we guarantee thread safety in Java?
What is thread safe?
A state in which synchronization is guaranteed when variable data is shared between threads.
What is the guarantee of synchronization?
Defined as satisfying the two points of "guarantee of atomicity" and "guarantee of visibility".
What is the guarantee of atomicity?
It is possible to exclude external interference from the start to the end of a certain process.
What is the guarantee of visibility?
The latest value can be referenced for the time series.
Especially in Java, the following control syntax is related to the elements that have appeared so far.
synchronized block
Synchronize operations between blocks that specify the same object as an argument.
A synchronized qualification for a method is equivalent to a synchronized block for its own instance.
//Synchronize operations between the following synchronized blocks
public void foo() {
synchronized (_SOME_OBJECT) {
// do something
}
}
public void bar() {
synchronized (_SOME_OBJECT) {
// do something
}
}
//The following are equivalent
public void foo() {
synchronized (this) {
// do something
}
}
public void synchronized foo() {
// do something
}
volatile modifier
Guarantee the visibility of the specified variable.
There are some situations that can be used for thread-safe purposes, but we will not deal with them this time.
For example, int type and boolean type variables are guaranteed to be atomic by language specifications.
Consider the case where foo () and bar () are called from different threads multiple times at irregular intervals for the following implementation.
public void foo() {
_object.update(); // _Processing that changes the internal state of object
}
public void bar() {
if (_object.enabled()) {
_object.getData(); // enabled()Exception occurs when called when is false
}
}
Since there is no guarantee of atomicity in the processing of bar (), the methods may be called in the following order. _object.enabled() -> _object.update() -> _object.getData()
public void foo() {
_object.update();
}
public void bar() {
if (_object.enabled()) {
_object.getData(); // _The internal state of object is not guaranteed
}
}
A guarantee of synchronization is added during the processing within the block, that is, the atomicity is also guaranteed. 【 _object.update() 】 -> 【 _object.enabled() -> _object.getData() 】 -> 【 _object.update() 】
It is easy to understand if you have an image that is stored in the processing queue in block units.
public void foo() {
synchronized (this) {
_object.update();
}
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _The internal state of object is guaranteed
}
}
}
Synchronize operations between blocks that specify the same object as an argument
The synchronized block is just a guarantee of synchronization as described above. The following implementation does not guarantee synchronization for the processing inside bar ().
public void foo() {
_object.update();
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _The internal state of object is not guaranteed
}
}
}
A supplement just in case. Synchronization is not guaranteed for operations between blocks that specify different objects as arguments.
public void foo() {
synchronized (_SOME_OBJECT) {
_object.update();
}
}
public void bar() {
synchronized (this) {
if (_object.enabled()) {
_object.getData(); // _The internal state of object is not guaranteed
}
}
}
In order to minimize the occurrence of deadlock, it is desirable to apply the following principles to the implementation.
this
Not desirable for principles as it is a public object to the user
Equivalent synchronized methods should not be used either
private class field
Guaranteed inaccessible to users
Not desirable for principles as it is an object shared between instances
private instance field
Guaranteed inaccessible to users
And since it is generated for each instance, it is optimal for the principle
Also, in either case the lock object must not be null. This is guaranteed by adding the final modifier.
//Recommended implementation
class Shared {
private final Object _LOCK_OBJECT = new Object(); //Don't make it static
public void foo() {
synchronized (_LOCK_OBJECT) {
// do something
}
}
public void bar() {
synchronized (_LOCK_OBJECT) {
// do something
}
}
}
class User {
public void sample() {
final Shared object = new Shared();
new Thread(() -> object.foo()).start();
new Thread(() -> object.bar()).start();
}
}
//Implementations that use this for lock objects
class Shared {
public void foo() {
synchronized (this) {
// do something
}
}
public void bar() {
synchronized (this) {
// do something
}
}
}
class User {
public void sample() {
final Shared object = new Shared();
//This object instance serves as a reference to the shared internal implementation's lock object.
//Therefore, it violates the principle of "minimizing the shared range of lock objects".
}
}
Similarly, according to the principle of minimizing the sharing range, a lock object should be defined for each target for which thread safety is to be guaranteed.
class Shared {
//Lock target
private final Data _dataA = new Data();
private final Data _dataB = new Data();
private final Data _dateC = new Data();
private final Validator = _validator = new Validator();
//Lock object
private final Object _DATA_A_LOCK = new Object();
private final Object _DATA_B_LOCK = new Object();
private final Object _DATA_C_LOCK = new Object();
public void foo() {
synchronized (_DATA_A_LOCK) {
_dataA.update();
}
synchronized (_DATA_B_LOCK) {
_dataB.update();
}
synchronized (_DATA_C_LOCK) {
_validator.execute();
_dataC.update();
}
}
public void bar() {
synchronized (_DATA_A_LOCK) {
_dataA.get();
}
synchronized (_DATA_B_LOCK) {
_dataB.get();
}
synchronized (_DATA_C_LOCK) {
_validator.execute();
_dataC.get();
}
}
}
Recommended Posts