Android—Synchronized and ReentrantLock

Basic use of Synchronized

1. Modify instance methods

public class SynchronizedMethods{
    
    private int sum = 0;

    public synchronized void calculate(){
        sum = sum + 1;
    }
}

The lock object in this case is the current instance object, so only calling this method on the same instance object will have a mutual exclusion effect; there will be no mutual exclusion effect between different instance objects. For example, the following code:

The above code calls the printLog() method with different objects in different threads, and there is no mutual exclusion. The running results are as follows: It can be seen that the two threads execute interactively.

Modify the above code as follows. Two threads call the printLog() method of the same object.

The execution effect is as follows. Only after the code in one thread is executed, the code in another thread will be called. At this time, the two threads are mutually exclusive.

2. Modify static methods

If synchronized modifies a static method, then the lock object is the Class object of the current class. Even if different instance objects are called in different threads, there will be a mutual exclusion effect. Modify the code as follows

The execution results are as follows. It can be seen that the two threads are still executed in sequence.

3. Modify code blocks

If synchronized modifies a code block, then the lock object is the object in brackets “()”. As can be seen from the following code, any Object object can look at the lock object

The execution results are as follows. It can be seen that the two threads are still executed in sequence.

Implementation details

synchronized can act on both methods and code blocks. But there are differences in implementation. For example, the following code uses synchronized to act on the code block

Use javap -c Foo to view the bytecode of the above code, as follows

It can be seen that the compiled bytecode contains two bytecode instructions, monitorenter and monitorexit.

Note: There are two monitorexits. The virtual machine needs to ensure that the lock can be released when an exception occurs, so there are two monitorexits, one is to release the lock after the code is executed normally, and the other is to release the lock when the code is executed abnormally.

synchronized modification method, as shown below. After the synchronized modified method is compiled into bytecode, it will be marked as ACC_SYNCHRONIZED in the flags attribute of the method. When a virtual machine accesses a method marked ACC_SYNCHRONIZED, monitorenter and monitorexit instructions are automatically added at the beginning and end of the method.

monitorenter and monitorexit can be understood as a specific lock, which holds two important attributes: counter and pointer.

Counter: represents how many times the current thread has accessed this lock;

Pointer: Points to the thread holding this lock.

Basic use of ReentrantLock

The use of ReentrantLock has different advantages from Synchronized. Its locking and unlocking need to be completed manually.

As shown in the above code, both ReentrantLock.lock() and ReentrantLock.unlock() need to be completed manually. The running effect is as follows. ReentrantLock can also achieve the same effect as Synchronized.

Note: The unlock() operation is placed in the finally code block because ReentrantLock does not automatically release the lock. When an exception occurs, it is ensured that the lock release operation will be executed (the code in finally can also be executed when an exception occurs. ). Synchronized will automatically release the lock when an exception occurs.

Fair lock implementation

ReentrantLock has a constructor with parameters as follows.

Both Synchronized and ReentrantLock are unfair locks by default, but ReentrantLock can create a fair lock by passing in a true value.

Fair lock: Through synchronization queue, multiple threads can acquire locks in the order in which they apply for locks.

Usage examples are as follows:

Read-write lock (ReentrantReadWriteLock)

In common development, a data structure shared between threads is often defined as a cache. For example, for a larger Map, all the corresponding relationships between city IDs and names are stored in the cache. This large Map provides read services most of the time, while write operations take up very little time. They are usually initialized when the server starts, and then the cached data can be refreshed at regular intervals. However, no other read operations can be performed between the start and end of the write operation, and the updated data after the write operation is completed needs to be visible to subsequent read operations.

Use the read-write lock (ReentrantReadWriteLock) in the concurrent package to implement the above functions. You only need to obtain the read lock during the read operation and the write lock during the write operation. When the write lock is acquired, subsequent read and write locks will be blocked. After the write lock is released, all operations continue to execute.

Use of read-write lock

1. Create a read-write lock

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

2. Obtain the read lock (ReadLock) and write lock (Write Lock) respectively through the read-write lock object.

ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.ReadLock writeLock = rwLock.writeLock();

3. Use read locks to cache read operations synchronously, and write locks to cache write operations synchronously.

//Read operation
readLock.lock();
try{
    //Read data from cache
} finally{
    readLock.unlock();
}

// write operation
writeLock.lock();
try{
    // Want to write data to the cache
} finally{
    writeLock.lock();
}

Implementation

As shown in the above code, the number in the picture is the data shared in the thread, used to simulate the cache data; at 1 in the picture, two Reader threads are created and read the data from the cache, and one Writer writes the data into the cache; Figure At 2 in the figure, a read lock (ReadLock) is used to lock the operation of reading data; at 3 in the figure, a write lock (WriteLock) is used to lock the operation of writing data into the cache.

Summary

● Two ways to achieve synchronization in Java, synchronized and ReentrantLock

● synchronized is easier to use, locking and releasing locks are automatically completed by the virtual machine

● ReentrantLock requires developers to complete it manually, and ReentrantLock has more usage scenarios.
Both Fair Lock and Read-Write Lock can play an important role in complex scenarios.