Concurrency control – Java locks

I’ve talked about two concurrency control approaches, optimistic locking and pessimistic locking. In the actual development process, in the multi-threaded development environment, also need to ensure the atomicity, visibility, order of multi-threading, at this time usually will introduce the locking mechanism, because my main development language is Java, so this time to mainly talk about the implementation and use of the lock in Java.

Java has two kinds of locks, respectively is Java’s built-in Synchronized keyword and Java. Introduced by Java1.5 util. Concurrent. The locks.

Synchronized

Synchronized is a built-in keyword in Java and is generally used to control the synchronization of key parts, so as to convert the concurrent task logic into serial processing logic. Similarly, Synchronized not only ensures data consistency, but also greatly reduces task processing efficiency. Therefore, in high performance, high concurrency, high traffic on the WEB server, for the Synchronized keyword, or can not use as far as possible.

Use the Synchronize:

/* * synchronize method block * lock obj object */
synchronized(obj){
    // do something
    globalVariable = ...
}
/* * The synchronization method is equivalent to locking the current object */
synchronized method(Object param){
    // do something
    globalVariable = ...
}
/* * synchronize static method * lock class */
static synchronized method(Object param){
    // do something
    globalVariable = ...
}
Copy the code
Tipuse
synchronizedLock does not need to be called because the Lock is automatically released when an exception is thrown in the block
lock.unlock();Before the lock is released

Principle of Synchronized

If you look at the compiled bytecode files of Synchronized, you can find two instructions, mointorenter and Mointorexit. It can be seen that all Jvm synchronization mechanisms are implemented based on Mointor objects, but it should be noted that The synchronized methods are not synchronized by monitorenter and Monitorexit directives, but are implicitly implemented by method invocation directives that read the ACC_SYNCHRONIZED flags of methods in the runtime constant pool.

java.util.concurrent.locks

Concurrent Concurrent packet issuing was introduced in java1.5 to address concurrency issues more elegantly. This article focuses on two of these locks, ReentrantLock ReentrantLock and ReadWriteLock read-write lock.

Before we look at reentrant and read-write locks in detail, we need to look at their common implementation of the Lock interface:

public interface Lock {
    // display lock
    void lock(a);

    // Get the lock, but respond to interrupt first
    void lockInterruptibly(a) throws InterruptedException;

    // Try to obtain the lock. If the lock is occupied, return false
    boolean tryLock(a);

    // Try to acquire the lock within x time. If the lock cannot be acquired within x time, abort
    boolean tryLock(long var1, TimeUnit var3) throws InterruptedException;

    // Display release lock
    void unlock(a);

    Condition newCondition(a);
}
Copy the code

Simple practices:

private static ReentrantLock lock = new ReentrantLock();
static int count; // Global variables

public static void main(String[] args){
    Runnable runnable = () -> {
        lock.lock();
        try{
            count ++;
        } finally{
            // Determine whether the lock holder is the current thread
            if(locak.isHeldByCurrentThread){ lock.unlock(); }}}; Thread thread1 =new Thread(runnable);
    Thread thread2 = new Thread(runnable);
    
    thread1.start();
    thread2.start();
}
Copy the code

It can be noted that, compared with synchronized keyword lock, lock lock and lock release are displayed. It should be noted that the lock will not be automatically released when the thread of obtaining the lock is abnormal. It is necessary to display the lock release in finally block, otherwise the lock may never be released and become a deadlock.

Interrupt () is a mechanism provided in the Java concurrency toolkit to deal with infinite waits. In synchronized locking, a thread enters the lock pool when the lock is occupied and waits for the lock holder to release the lock. Thread.interrupt () is called after the lock is Interrupted. Threads do not respond immediately. At this time, if the thread that holds the lock is deadlocked, a large number of threads will be overstocked in the lock pool and cannot be released.

Practice:

private static ReentrantLock lock = new ReentrantLock();

public static void main(String[] args) throws Exception{
	Runnable runnable = () -> {
		try{
			lock.lockInterruptibly();
			Thread.sleep(30000); // Simulate deadlock
		}catch(InterruptedException e){
			System.out.printf("thread interrupted %s \n", Thread.currentThread().getName());
		}finally{
			 if(lock.isHeldByCurrentThread()){ lock.unlock(); }}}; Thread thread1 =new Thread(runnable);
	Thread thread2 = new Thread(runnable);
	
	thread1.start(); Thread1 will acquire the lock
	thread2.start(); // Thread2 will enter the wait
	Thread.sleep(500);
	thread2.interrupt(); // The thread2 response is interrupted
}
Copy the code

The Timeout mechanism is also a way to solve the problem of locking the infinite blocking thread. This function is relatively simple to use, and can be implemented through the tryLock() method of Lock and tryLock(long Timeout, TimeUtil TimeUtil) method. The tryLock() method gives up the lock contention after the specified time.

ReentrantLock constructor

public ReentrantLock(a) {
	this.sync = new ReentrantLock.NonfairSync();
}

public ReentrantLock(boolean var1) {
	this.sync = (ReentrantLock.Sync)(var1 ? new ReentrantLock.FairSync() : new ReentrantLock.NonfairSync());
}
Copy the code

If true, the constructor uses a fair lock. By default, it uses an unfair lock.

  • Fair lock
    • Assign lock permissions based on the wait time
      • Fair locking equalizes time slices, reduces system throughput, but ensures that threads are executed sequentially
  • Not fair lock
    • Randomly assign lock permissions

ReetrantLock

A reentrant lock is a concrete implementation of the Java Concurrency Toolkit lock interface. It is called reentrant lock because the lock can be acquired multiple times by a thread. Of course, the lock must be released as many times as it is acquired. If the lock is released less times, it will not be able to leave the lock boundary and other threads will not be able to acquire the lock. Release number is much, will throw IllegalMonitorStateException.

Simple practices:

public class ReentrantLockExample{
    private static Lock lock = new ReentrantLock();
    static int count;

    public static void main(String[] args){
        ReentrantLockExample example = new ReentrantLockExample();

        Runnable runnable = () -> {
            example.executeThings();
        };

        Thread testThread = new Thread(runnable);
        testThread.start();
    }

    void executeThings(a){
        lock.lock();
        count++;
        if(count == 10){
            lock.unlock();
        }else{ executeThings(); lock.unlock(); }}}Copy the code

According to the practical logic, the instance thread recursively executes the executeThings() method, thus acquiring the lock multiple times, and recursively releasing the lock multiple times after reaching the execution boundary. If a reentrant lock cannot be reentrant, it will generate a deadlock with itself on the first recursion, which is logically not allowed, which is one of the meanings of a reentrant lock.

Condition

Condition is the lock monitoring method, used to replace the wait(), notify(), and notifyAll() methods of objects.

Let’s look at the Condition interface’s methods:

public interface Condition {
    void await(a) throws InterruptedException;

    void awaitUninterruptibly(a);

    long awaitNanos(long var1) throws InterruptedException;

    boolean await(long var1, TimeUnit var3) throws InterruptedException;

    boolean awaitUntil(Date var1) throws InterruptedException;

    void signal(a);

    void signalAll(a);
}
Copy the code

As you can see, the Condition interface’s methods correspond to Object’s thread control methods.

Simple practices:

private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();

public static void main(String[] args){
        Runnable workThread = () -> {
            lock.lock();
            try {
                System.out.println("ready to await");
                condition.await();
                System.out.println("keep running");
            } catch (InterruptedException e) {
                // Do nothing
            } finally {
                if(lock.isHeldByCurrentThread()){ lock.unlock(); }}}; Runnable singleThread = () -> {// Execute the wake up method after acquiring the lock
            lock.lock();
            condition.signal();
            lock.unlock();
        };

        Thread threadTest0 = new Thread(workThread);
        Thread threadTest1 = new Thread(singleThread);

        threadTest0.start();
        Thread.sleep(3000);
        threadTest1.start();
}

/* * output: * ready to await * 3 seconds later * keep running */
Copy the code

ReadWriteLock(read-write lock)

Generally speaking, the read demand for data is much larger than the write demand, and it may not be necessary to lock data when operating data, or even read content does not need to lock at all. Therefore, in the Java Concurrency toolkit, we also provide read and write lock, read and write separation, to improve efficiency.

Simple practices:

 private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();

private static volatile int value;

public static void main(String[] args) throws Exception{
	Callable<Integer> readThread  = () -> {
		readLock.lock();
		try{
			// simulate file reading
			Thread.sleep(100);
		}catch(InterruptedException e){
			// do nothing
		}finally {
			readLock.unlock();
		}
		return value;
	};

	Runnable writeThread = () -> {
		writeLock.lock();
		try{
			value += 2;
		}finally{ writeLock.unlock(); }}; }Copy the code

conclusion

In Java locks, Synchronized and Java. Util. Concurrent. The locks here even finished talking about, roughly Synchronized can gradually replaced by the locks. Although the conceptual and simple usage can be understood through this article, the problems encountered in actual concurrent development are too varied. Try to combine concurrent design patterns, caches, message queues, locks, and other tools to solve concurrency problems to the maximum extent possible.

Ps: My personal blog address is Shawjie. me. I will release some of my experience, learning and understanding from time to time.