011Java thread synchronization and locking

Posted by mallen on Sat, 09 Oct 2021 04:39:20 +0200

Some of the content comes from the following Blogs:

https://www.cnblogs.com/dolphin0520/p/3923167.html

https://blog.csdn.net/tyyj90/article/details/78236053

1. Thread synchronization mode

For thread safety, we used the synchronized keyword earlier. For thread collaboration, we used Object.wait() and Object.notify(). In JDK1.5, java provides us with Lock to realize the same functions as them, and the performance is better than them. In JDK1.6, JDK optimizes synchronized, and there is little difference in performance between the two methods.

1.1 synchronized defects

synchronized modified code block. When a thread obtains the corresponding lock and executes the code block, other threads can only wait all the time. The thread waiting to obtain the lock releases the lock. If it does not release, it needs to wait indefinitely.

There are only two situations when the thread that obtains the lock releases the lock:

1) The thread that obtains the lock executes the code block, and then the thread releases its possession of the lock.

2) When an exception occurs in thread execution, the JVM will let the thread automatically release the lock.

To sum up, Lock provides more functions than synchronized. However, the following points should be noted:

1) Lock is not built-in in the Java language. synchronized is a keyword of the Java language, so it is a built-in feature. Lock is a class that enables synchronous access.

2) Synchronized does not need to release the lock manually. After the synchronized method or synchronized code block is executed, the system will automatically let the thread release the occupation of the lock. Lock requires the user to release the lock manually. If the lock is not released actively, it may lead to deadlock.

1.2 Lock

The Lock interface is located in the java.util.concurrent.locks package.

public interface Lock {
    // Used to acquire locks. Wait if the lock has been acquired by another thread.
    void lock();

    // Used to acquire locks. It allows other threads to call the interrupt method to interrupt the wait and return directly when waiting. At this time, an InterruptedException will be thrown instead of obtaining the lock.
    void lockInterruptibly() throws InterruptedException;

    // It is used to attempt to acquire the lock. If the acquisition is successful, it returns true. If the acquisition fails (that is, the lock has been acquired by other threads), it returns false.
    boolean tryLock();

    // It is used to try to obtain the lock. If the lock is obtained or obtained during the waiting period, it returns true. If the acquisition fails within a certain period of time, false is returned.
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    // Release the lock.
    void unlock();

    // Gets the Condition object.
    Condition newCondition();
}

1.2.1 lock method

First, the lock() method is the most commonly used method, which is used to obtain locks. Wait if the lock has been acquired by another thread.

As mentioned earlier, if Lock is used, the Lock must be released actively, and the Lock will not be released automatically in case of abnormality. Therefore, generally speaking, the use of Lock must be carried out in the try{}catch {} block, and the operation of releasing the Lock must be carried out in the finally block to ensure that the Lock must be released and prevent deadlock.

Generally, Lock is used for synchronization in the following form:

Lock lock = ... ;
lock.lock();
try {
    // Processing tasks
} catch(Exception e) {

} finally {
    lock.unlock();// Release lock
}

1.2.2 tryLock method

The tryLock() method has a return value, which indicates that it is used to attempt to acquire the lock. If the acquisition is successful, it returns true. If the acquisition fails (that is, the lock has been acquired by other threads), it returns false, that is, this method will return immediately anyway. I won't wait there when I can't get the lock.

The tryLock(long time, TimeUnit unit) method is similar to the tryLock() method, except that this method will wait for a certain time when the lock cannot be obtained. If the lock cannot be obtained within the time limit, it will return false. If the lock is obtained at the beginning or during the waiting period, return true.

Generally, tryLock is used to obtain locks:

Lock lock = ... ;
if (lock.tryLock()) {
    try {
        // Processing tasks
    } catch (Exception e) {

    } finally {
        lock.unlock();// Release lock
    }
} else {
    // Get failed to handle other things
}

1.2.3 lockinterruptible method

The lockInterruptibly() method is special. When obtaining A lock through this method, if the thread is waiting to obtain A lock, the thread can respond to an interrupt, that is, interrupt the waiting state of the thread. That is to say, when two threads want to obtain A lock through lock. Lockinterrupt() at the same time, if thread A obtains the lock and thread B is only waiting, calling threadB.interrupt() method on thread B can interrupt the waiting process of thread B.

Because an exception was thrown in the declaration of lockinterruptible(), lock. Lockinterruptible() must be placed in the try block or declared to throw an InterruptedException outside the method calling lockinterruptible().

The general forms of use are as follows:

public void method() throws InterruptedException {
    Lock lock = ... ;
    lock.lockInterruptibly();
    try {
        // Processing tasks
    } finally {
        lock.unlock();
    }
}

Note that when a thread acquires a lock, it will not be interrupted by the interrupt() method. As mentioned in the previous article, calling the interrupt() method alone can not interrupt the running thread, but only the thread in the blocking process.

Therefore, when a lock is obtained through the lockinterruptable () method, if it cannot be obtained, it can respond to the interrupt only when waiting.

If it is modified with synchronized, when a thread is in the state of waiting for a lock, it cannot be interrupted, and only wait all the time.

1.3 ReentrantLock

The ReentrantLock class implements the Lock interface, and ReentrantLock provides more methods.

public class Demo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {ticket.sale();}, "Window 1").start();
        new Thread(() -> {ticket.sale();}, "Window 2").start();
    }
}

class Ticket {
    private int num = 3;
    Lock lock = new ReentrantLock();

    public void sale() {
        while (num > 0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.lock();
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + " sale " + num--);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

Note that when declaring Lock, be careful not to declare it as a local variable.

1.4 ReadWriteLock

ReadWriteLock is also an interface used to define read-write locks.

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

One is used to obtain a read lock and the other is used to obtain a write lock. In other words, the file read and write operations are separated and divided into two locks to be allocated to threads, so that multiple threads can read at the same time.

1.5 ReentrantReadWriteLock

ReentrantReadWriteLock implements the ReadWriteLock interface and supports multiple threads to read at the same time.

public class Demoo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {ticket.sale();}, "Window 1").start();
        new Thread(() -> {ticket.sale();}, "Window 2").start();
        new Thread(() -> {ticket.sale();}, "Window 3").start();
        new Thread(() -> {ticket.sale();}, "Window 4").start();
    }
}
class Ticket {
    private int num = 3;
    ReadWriteLock lock = new ReentrantReadWriteLock();

    public void show() {
        while (num > 0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.readLock().lock();
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + " show " + num);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.readLock().unlock();
            }
        }
        System.out.println(Thread.currentThread().getName() + " end");
    }

    public void sale() {
        while (num > 0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            lock.writeLock().lock();
            try {
                if (num > 0) {
                    System.out.println(Thread.currentThread().getName() + " sale " + num--);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.writeLock().unlock();
            }
        }
        System.out.println(Thread.currentThread().getName() + " end");
    }
}

The operation results are as follows:

Window 2 show 3
 Window 3 show 3
 Window 1 show 3
 Window 2 show 3
 Window 3 show 3
 Window 1 show 3
 Window 4 sale 3
 Window 2 show 2
 Window 3 show 2
 Window 1 show 2
 Window 4 sale 2
 Window 2 show 1
 Window 1 show 1
 Window 4 sale 1
 Window 4 end
 Window 3 end
 Window 2 end
 Window 1 end

From the running results, there are at most three threads reading at the same time, which improves the efficiency of reading operation.

If a thread has occupied a read lock, if other threads want to apply for a write lock, the thread applying for a write lock will wait to release the read lock.

If a thread has occupied a write lock, if other threads apply for a write lock or a read lock, the requesting thread will wait for the write lock to be released.

1.6 degradation of read-write lock

A write lock can be downgraded to a read lock, but a read lock cannot be upgraded to a write lock.

Write locks can be downgraded to read locks:

public static void main(String[] args) {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    // Lock degradation
    try {
        writeLock.lock();
        System.out.println("Get write lock");
        readLock.lock();
        System.out.println("Acquire read lock");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        writeLock.unlock();
        System.out.println("Release write lock");
        readLock.unlock();
        System.out.println("Release read lock");
    }
}

The program can be executed normally:

Get write lock
 Acquire read lock
 Release write lock
 Release read lock

Read lock unavailable upgrade to write lock:

public static void main(String[] args) {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    // Lock upgrade
    try {
        readLock.lock();
        System.out.println("Acquire read lock");
        writeLock.lock();
        System.out.println("Get write lock");
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        readLock.unlock();
        System.out.println("Release read lock");
        writeLock.unlock();
        System.out.println("Release write lock");
    }
}

The program blocks and waits:

Acquire read lock

1.7 comparison between synchronized and Lock

1) Lock is an interface, synchronized is a keyword in Java, and synchronized is a built-in language implementation.

2) synchronized will automatically release the Lock held by the thread when an exception occurs, so it will not cause deadlock. When an exception occurs to Lock, if the Lock is not actively released, it is likely to cause deadlock. Therefore, when using Lock, you need to release the Lock in the finally block.

3) Lock allows the thread waiting for the lock to respond to the interrupt, but synchronized does not. When synchronized is used, the waiting thread will wait all the time and cannot respond to the interrupt.

4) Through Lock, you can know whether you have successfully obtained the Lock, but synchronized cannot.

5) Lock can improve the efficiency of reading operations by multiple threads.

6) The bottom layer of synchronized is a waiting queue based on CAS operation. Synchronized also implements spin Lock and is optimized for different systems and hardware systems, while Lock completely relies on the system to block and suspend waiting threads.

7) When the resource competition is not very fierce, the performance of synchronized is better than that of ReetrantLock. However, when the resource competition is very fierce, the performance of synchronized will decline dozens of times, but the performance of ReetrantLock can remain normal.

2 classification of locks

In many concurrent articles, I will mention various locks, such as fair locks, optimistic locks, etc. This article introduces the classification of various locks. The contents are as follows:

Reentrant lock
Exclusive lock / shared lock
Mutex / read / write lock
Fair lock / unfair lock
Optimistic lock / pessimistic lock
Sectional lock
Spin lock

The above are many lock nouns. These classifications do not all refer to the state of the lock. Some refer to the characteristics of the lock and some refer to the design of the lock. The following summary is to explain the nouns of each lock.

2.1 reentrant lock

Reentrant lock, also known as recursive lock, means that when the same thread obtains a lock in the outer method, it will automatically obtain the lock when entering the inner method.

Both synchronized and ReentrantLock are reentrant locks.

One advantage of reentrant lock is that it can avoid deadlock to a certain extent. If it is not reentrant lock, it may cause deadlock.

2.2 exclusive lock / shared lock

An exclusive lock means that the lock can only be held by one thread at a time. A shared lock means that the lock can be held by multiple threads.

For synchronized and ReentrantLock, they are exclusive locks.

However, for ReadWriteLock, its read lock is a shared lock and its write lock is an exclusive lock. The shared lock of read lock can ensure that concurrent reading is very efficient. The processes of reading, writing, reading and writing are mutually exclusive.

Exclusive locks and shared locks are also realized through AQS. Exclusive locks or shared locks can be realized through different methods.

2.3 mutex / read / write lock

The exclusive lock / shared lock mentioned above is a broad term, and the mutex lock / read-write lock is a specific implementation.

The specific implementation of mutex in Java is ReentrantLock. The specific implementation of read-write lock in Java is ReadWriteLock.

2.4 fair lock / unfair lock

Fair lock means that multiple threads acquire locks in the order of applying for locks. Unfair lock means that multiple threads acquire locks in the order of applying for locks, not in the order of applying for locks. The advantage of non fair lock is that the throughput is greater than that of fair lock.

For synchronized, it is an unfair lock.

For ReentrantLock, specify whether the lock is a fair lock through the constructor. The default is a non fair lock.

2.5 optimistic lock / pessimistic lock

Optimistic locks and pessimistic locks do not refer to specific types of locks, but to the perspective of concurrent synchronization.

Pessimistic locks believe that concurrent operations on the same data must be modified. Even if they are not modified, they will be considered modified. Therefore, pessimistic locking takes the form of locking for concurrent operations of the same data. Pessimists believe that there will be problems with concurrent operations without locks. The use of pessimistic locks in Java is to use various locks.

Optimistic lock holds that concurrent operations on the same data will not be modified. When updating data, it will try to update and constantly re update the data. I am optimistic that there is nothing wrong with concurrent operations without locks. The use of optimistic lock in Java is lock free programming. CAS algorithm is often used. A typical example is atomic class, which realizes the update of atomic operation through CAS spin.

2.6 sectional lock

Segmented lock is actually a lock design, not a specific lock. For ConcurrentHashMap before JDK1.8, its concurrent implementation is to realize efficient concurrent operation in the form of segmented lock.

The design purpose of segmented lock is to refine the granularity of lock. When the operation does not need to update the whole array, only one item in the array is locked.

2.7 spin lock

In Java, spin lock means that the thread trying to obtain the lock will not block immediately, but will try to obtain the lock in a loop. This has the advantage of reducing the consumption of thread context switching, but the disadvantage is that the loop will consume CPU.

3 Classification of locks

It refers to the status of the lock, which is indicated by the field in the object header of the object monitor. There are four kinds: no lock, bias lock, lightweight lock and heavyweight lock.

The lock level can only be upgraded, not downgraded. The purpose of this strategy is to improve the efficiency of obtaining and releasing locks.

The unlocked state is that the synchronization code is not accessed by any thread. At this time, it is unlocked.

Biased lock means that a piece of synchronous code has been accessed by a thread, and the thread will automatically obtain the lock. Reduce the cost of obtaining locks.

Lightweight lock means that when a lock is a biased lock and is accessed by another thread, the biased lock will be upgraded to a lightweight lock. Other threads will try to obtain the lock in the form of spin without blocking and improving performance.

Heavyweight lock means that when the lock is a lightweight lock, although another thread spins, the spin will not continue all the time. When it spins a certain number of times, it will enter blocking before it has obtained the lock, and the lock will expand into a heavyweight lock. The heavyweight lock will block the threads of other applications and reduce the performance.

4 LockSupport

4.1 definitions

LockSupport is a basic thread blocking primitive used to create locks and other synchronization classes. It uses the concept of a license internally to realize blocking and wake-up. Each thread has a license. The license has only two values: 1 and 0. The default is 0.

park() and unpark() in LockSupport are used to block and unblock threads respectively. The park() method will execute when the license is available, and the unpark() method will provide the license when the license is not available.

4.2 three thread communication modes and their limitations

1) Use the wait() method in Object to make the thread wait, and use the notify() method in Object to wake up the thread.

It must be executed inside synchronized, otherwise the IllegalMonitorStateException is thrown.

The wait() method must be executed before the notify() method, that is, the notify() method can only be used to wake up after the wait() method, otherwise the program will wait and cannot be awakened.

2) Use Condition's await() method to make the thread wait, and use Condition's signal() method to wake up the thread.

Must be queued with Lock, otherwise the IllegalMonitorStateException will be thrown.

The notify() method can only be used to wake up after the wait() method, otherwise the program will wait all the time and cannot be awakened.

3) Use LockSupport's park() method to make the thread wait, and use LockSupport's uppark() method to wake up the thread.

There is no need to execute in any method block. The park() method and uppark() method are static methods and can be executed anywhere.

There is no requirement for the order of use. Even if the uppark() method is advanced, it can ensure that the park() method is awakened.

4.3 use

The code is as follows:

public static void main(String[] args) {
    Thread a = new Thread(()->{
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "-----get into");
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + "-----implement");
    }, "a");
    a.start();
    Thread b = new Thread(()->{
        System.out.println(Thread.currentThread().getName() + "-----get into");
        LockSupport.unpark(a);
        System.out.println(Thread.currentThread().getName() + "-----implement");
    }, "b");
    b.start();
}

The results are as follows:

b-----get into
b-----implement
// Wait three seconds
a-----get into
a-----implement

4.4 description

LockSuport is a thread blocking tool class. All methods are static methods that allow threads to block and wake up at any position. The native code in Unsafe called by LockSupport.

Call the park() method. If the voucher is 1, the voucher will be changed to 0. At the same time, the park() method will immediately return and continue to execute. If the voucher is 0, it will be blocked until the unpark() method is executed to change the voucher from 0 to 1.

Calling the unpark() method will change the voucher from 0 to 1. The result of calling unpark() multiple times will still make the voucher 1, which will not lead to voucher accumulation. Vouchers have only two values: 0 and 1.

Topics: Java