Java multithreading: take you to know different locks!

Posted by PHP'er on Thu, 10 Feb 2022 00:05:11 +0100

I Lock interface

Lock interface is the foundation of everything. Its abstract class is a tool used to control the access of multiple threads to shared resources

The following methods are provided to abstract the whole business:

  • void lock()
  • void lockInterruptibly() throws InterruptedException: breaks the lock
  • boolean tryLock(): non blocking attempt to acquire a lock
  • Boolean trylock (long time, timeunit) throws interruptedexception: attempts with time
  • void unlock()
  • Condition newCondition()

Lock interface provides more functions different from implicit monitoring lock:

  • Guaranteed sorting
  • Non reentrant use
  • Deadlock detection
  • Itself can be used as a target in a synchronization statement
  • There is no specified relationship between obtaining the monitor lock of the lock instance and any lock() method calling the instance

Memory synchronization:

  • A successful lock operation has the same memory synchronization effect as a successful lock operation.
  • A successful unlocking operation has the same memory synchronization effect as a successful unlocking operation.
  • Unsuccessful lock and unlock operations and reentrant lock / unlock operations do not require any memory synchronization effect.

II ReentranLock

2.1 introduction to reentranlock

ReentranLock is a re-entry lock, which means that the lock can be entered repeatedly in a single thread, that is, a thread can obtain the same lock twice in a row

ReentranLock provides more extensive lock operations than synchronized. It allows more flexible structures, can have completely different properties, and can support conditional objects of multiple related classes.

Its advantages include:

  • It can make the lock more fair.
  • Recursive non blocking synchronization mechanism.
  • This allows the thread to respond to an interrupt while waiting for a lock.
  • You can let the thread try to acquire the lock and return immediately or wait for a period of time when the lock cannot be acquired.
  • Locks can be acquired and released in different ranges and in different orders.

Its features are:

  • Reentrant mutex
  • Provide both fair and unfair ways

Fair lock: the lock acquisition of fair lock is sequential

ReentranLock basic use

private Lock lock = new ReentrantLock();

public void test() {
    lock.lock();
    for (int i = 0; i < 5; i++) {
    logger.info("------> CurrentThread [{}] , i : [{}] <-------", Thread.currentThread().getName(), i);
    }
    lock.unlock();
}

2.2 internal important categories

2.2.1 Sync

Sync is an internal abstract class of ReentranLock, which will be used to implement two different locks later. Let's take a look at what is done inside sync

Node 1: inherited from AbstractQueuedSynchronizer

It is also known as AQS. Sync uses aqs state to represent the number of locks held

abstract static class Sync extends AbstractQueuedSynchronizer

Node 2: there is an abstract method lock, and subsequent fair and unfair methods will implement the corresponding methods respectively

abstract void lock();
// ?-  Synchronization object of unfair lock
static final class NonfairSync extends Sync
> Distinguishing method : final void lock() : Compared with fair lock, there is a modification state Operation of , If the modification is successful, set the thread that currently has exclusive access
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());

// ?-  Fair lock synchronization object 
static final class FairSync extends Sync
> Distinguishing method : tryAcquire(int acquires) , The biggest difference is that it will query whether there are threads waiting for acquisition longer than the current thread

Node 3: what does the nonfairtryacquire method do

       
       final boolean nonfairTryAcquire(int acquires) {
            // Get current Thread and status
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                // CAS setting status
                if (compareAndSetState(0, acquires)) {
                    // Sets the thread that currently has exclusive access
                    // null indicates that no thread has obtained access
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // Returns the last thread set by setExclusiveOwnerThread
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }//Welcome to learn and communicate in Java. Sample: 222578377 communicate and blow water together~

Node 3: tryrelease

// Important, you can see two operations: setExclusiveOwnerThread + setState
// setExclusiveOwnerThread is null, indicating that no thread has obtained access rights

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

2.3 similarities and differences between synchronized and ReentrantLock

Same point

  • Both implement multithreading synchronization and memory visibility semantics (implicit monitor locking).
  • Are reentrant locks

difference

  • Different synchronization mechanisms

Synchronized is synchronized with the Monitor object through the Java object header lock tag.

ReentrantLock realizes synchronization through CAS, AQS (AbstractQueuedSynchronizer) and LockSupport (for blocking and unblocking).

  • Different visibility implementation mechanisms

synchronized relies on the JVM memory model to ensure the visibility of multithreaded memory containing shared variables.

ReentrantLock ensures the visibility of multithreaded memory containing shared variables through the volatile state of AQS.

  • Different ways of use

synchronized can modify instance methods (lock instance objects), static methods (lock class objects), and code blocks (display specified lock objects).

ReentrantLock shows that when tryLock and lock methods are called, the lock needs to be released in the finally block.

  • Different functional richness

synchronized can not set the waiting time and can not be interrupted.

ReentrantLock provides rich functions such as limited time waiting lock (setting expiration time), lockinterruptible lock, condition (providing await, condition (providing await, signal and other methods), etc

  • Different lock types

synchronized only supports non fair locks.

ReentrantLock provides fair lock and non fair lock implementations. Of course, in most cases, unfair locking is an efficient choice.

Summary:

Before synchronized optimization, its performance was much worse than that of ReenTrantLock, but since synchronized introduced bias lock and lightweight lock (spin lock), their performance has been almost the same

When both methods are available, officials even recommend using synchronized.

Moreover, in the actual code battle, the possible optimization scenario is to further improve the performance through read-write separation, so ReentrantReadWriteLock is used [References]

2.3 ReentrantLock

// Common methods:
- void lock()
- Condition newCondition()
- boolean tryLock()
- void unlock()  

--------------
// Node 1: it is based on the Lock interface and supports serialization
ReentrantLock implements Lock, java.io.Serializable

--------------
// Node 2: internal class. There are several important sync classes in ReentrantLock. Sync is the basis of synchronization control


--------------
// Node 3: fair and unfair handover mode
 public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
}

--------------
// Node 4: implementation of the lock method, which calls NonfairSync by default
 public void lock() {
    sync.lock();
}


--------------
// Node 5: implementation of lockinterruptible
sync.acquireInterruptibly(1);

// Node 6 : 
public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
}
     
     

III ReadWriteLock

Read write Lock is a Lock implementation class of Lock separation technology used to improve the performance of concurrent programs. It can be used in the scenario of "more reads and less writes". The read-write Lock supports multiple read operations to be executed concurrently, and the write operation can only be operated by one thread.

ReadWriteLock optimizes the situation that writes to the data structure relatively infrequently, but multiple tasks need to read the data structure frequently.

ReadWriteLock allows you to have multiple readers at the same time, as long as they don't try to write. If the write lock is already held by another task, no reader can access it until the write lock is released.

The improvement of ReadWriteLock on program performance is mainly subject to the following factors:

  • The result of comparing the frequency at which data is read with the modified frequency.
  • Read and write time
  • How many threads compete
  • Is it running on a multiprocessing machine

features:

  • Fairness: support fairness and unfairness.
  • Reentry: reentry is supported. Read / write locks support up to 65535 recursive write locks and 65535 recursive read locks.
  • Lock demotion: follow the order of acquiring write locks, acquiring read locks, and finally releasing write locks, so that write locks can be demoted into read locks.

Drill down to ReadWriteLock:

ReadWriteLock Is an interface , It provides only two methods : 

    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();

IV ReentrantReadWriteLock

Reentrant lock ReentrantLock is an exclusive lock. Only one thread can access the exclusive lock at the same time. ReentrantReadWriteLock is an implementation class of reentrant read-write lock. As long as there is no thread writer, the read lock can be maintained by multiple Reader threads at the same time

I- ReadWriteLockM- Lock readLock();M- Lock writeLock();

C- ReentrantReadWriteLock: reentrant read-write lock implementation class I- ReadWriteLock
?- A pair of related locks are maintained internally, one for read-only operation and the other for write operation. The write lock is exclusive and the read lock is shared [References]

4.1 ReentrantReadWriteLock

Use case:

    Object data;
    volatile boolean cacheValid;
    final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

    void processCachedData() {
        rwl.readLock().lock();
        if (!cacheValid) {
            rwl.readLock().unlock();
            rwl.writeLock().lock();
            try {
                if (!cacheValid) {
                    data = "test";
                    cacheValid = true;
                }
                rwl.readLock().lock();
            } finally {
                rwl.writeLock().unlock(); // Unlock write, still hold read
            }
        }
    }

Node 1: two internal attributes are provided internally, which is why the write only lock can be separated

// Read lock
private final ReentrantReadWriteLock.ReadLock readerLock;
// Write lock
private final ReentrantReadWriteLock.WriteLock writerLock;

Node 2: once again, the old rule of Sync is to judge the creation of Sync through fair

final Sync sync;
public ReentrantReadWriteLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
	readerLock = new ReadLock(this);
	writerLock = new WriteLock(this);
}//Welcome to learn and communicate in Java. Sample: 222578377 communicate and blow water together~

Node 3: sync internal state control

// Read and write counts extract constants and functions. Lock state is logically divided into two:
// A lower (low 16) indicates the exclusive (write) lock hold count, and a higher (high 16) indicates the shared (read) lock hold count.
static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

Counting method : 

// Number of threads that acquire locks that hold read status
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
Read status, equal to S >>> 16 (Unsigned complement 0 shift right 16 bits)
    
// The number of times a lock holding a write state was obtained
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } 
Write status, equal to S & 0x0000FFFF(Erase all the upper 16 bits)

Node 4: the function of holdcounter class: each read thread needs a separate count for reentry

// Each thread reads the counter holding the count. As ThreadLocal maintenance, the cache is stored in cachedHoldCounter 
static final class HoldCounter {
	int count = 0;
	// Non references help with garbage collection
	final long tid = getThreadId(Thread.currentThread());
}
// Successfully obtained the hold count of the last thread of readLock
private transient HoldCounter cachedHoldCounter;


Node 5: threadlocalholdcounter, for deserialization mechanism

static final class ThreadLocalHoldCounter
	extends ThreadLocal<HoldCounter> {
	public HoldCounter initialValue() {
		return new HoldCounter();
	}
}
// The number of reentrant read locks held by the current thread. Initialize only in constructors and readobjects. Delete when the read hold count of the thread drops to 0
private transient ThreadLocalHoldCounter readHolds;

Node 6: sync internal class

NonfairSync : Unfair lock
final boolean writerShouldBlock() {
	return false; // writers can always barge
}
final boolean readerShouldBlock() {
	// If the thread that temporarily appears at the head of the queue (if present) is a waiting writer, it is blocked
    // If there is a waiting writer behind another enabled reader that has not been exhausted from the queue, the new reader will not block
	return apparentlyFirstQueuedIsExclusive();
}

FairSync : Fair lock
static final class FairSync extends Sync {
	final boolean writerShouldBlock() {
		return hasQueuedPredecessors();
	}
	final boolean readerShouldBlock() {
		return hasQueuedPredecessors();
	}
}


V Condition

5.1 introduction to condition

After Java SE 5, Java provides a Lock interface. Compared with synchronized, Lock provides conditional conditions, which makes the waiting and wake-up operations of threads more detailed and flexible

AQS waiting queue and Condition queue are two independent queues
#await() is to release the lock resource based on the lock held by the current thread, create a new Condition node and add it to the end of the Condition queue to block the current thread. [References]

#signal() is to move the head node of Condition to the tail of AQS waiting node to wait for it to acquire the lock again.

5.2 Condition process


5.3 Condition source code

Condition is actually an interface. In AQS, there is an implementation class, ConditionObject. Let's mainly talk about it:

Node 1: attribute object

// condition queue first node
private transient Node firstWaiter;
// condition queue last node
private transient Node lastWaiter;

Node 2: core method doSignal + doSignalAll

// doSignal: delete and transfer nodes until non cancelled 1 or null is encountered
private void doSignal(Node first) {
    do {
        // First determine whether it is a header node or null
        // Note that = is the assignment:! transferForSignal(first) &&(first = firstWaiter) !=  null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
            first.nextWaiter = null;
    // transferForSignal is an AQS method that transfers a node from the condition queue to the synchronization queue. It is mainly the CAS operation modification status. / / welcome to learn and communicate in Java. Sample: 222578377 communicate and blow water together~
    // Node p = enq(node);  This is a node splicing operation. In fact, it can be understood that the node has been added to the corresponding queue
    } while (!transferForSignal(first) &&(first = firstWaiter) != null);
}



// doSignalAll: delete and transmit all nodes. Pay attention to the difference. Unlike Notify, there is a thread to get the notification
private void doSignalAll(Node first) {
    lastWaiter = firstWaiter = null;
    do {
        Node next = first.nextWaiter;
        first.nextWaiter = null;
        transferForSignal(first);
        first = next;
    } while (first != null);
}
        

Node 3: main method await

public final void await() throws InterruptedException {
	if (Thread.interrupted())
		throw new InterruptedException();
    // Step 1: add to Condition queue
	Node node = addConditionWaiter();
    // Step 2: call release with the current status value and return the saved status
	int savedState = fullyRelease(node);
	int interruptMode = 0;
    // Returns true if a node (always the node originally placed on the condition queue) is now waiting for re acquisition on the synchronization queue
    // If there is a waiting node
	while (!isOnSyncQueue(node)) {
		LockSupport.park(this);
		if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
			break;
	}
    // Gets the thread already in the queue in exclusive non interruptible mode.
    // Used for conditional waiting method and acquisition method.
	if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
		interruptMode = REINTERRUPT;
	if (node.nextWaiter != null) // clean up if cancelled
		unlinkCancelledWaiters();
	if (interruptMode != 0)
		reportInterruptAfterWait(interruptMode);
}

// awaitNanos(long nanosTimeout): timed conditional wait
    if (nanosTimeout <= 0L) {
		transferAfterCancelledWait(node);
		break;
	}
	if (nanosTimeout >= spinForTimeoutThreshold)
		LockSupport.parkNanos(this, nanosTimeout);
	if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
		break;
	nanosTimeout = deadline - System.nanoTime();


// awaitUntil(Date deadline): realize absolute timing condition waiting, that is, a timing operation 
// Direct transfer node after timeout
	if (System.currentTimeMillis() > abstime) {
		timedout = transferAfterCancelledWait(node);
		break;
	}

Node 4: release method

    // Call release with the current status value; Returns the saved state.
    // Cancel the node and throw an exception on failure.
    final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            // Get status
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                // Set current status to cancel
                node.waitStatus = Node.CANCELLED;
        }//Welcome to learn and communicate in Java. Sample: 222578377 communicate and blow water together~
    }


    public final boolean release(int arg) {
        // Implement specific rewriting
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


Node 5: other methods:

method addConditionWaiter : Added a new waiting queue for waiters.
awaitUninterruptibly :Realize non interruptible condition waiting
//Welcome to learn and communicate in Java. Sample: 222578377 communicate and blow water together~
public final void awaitUninterruptibly() {
	Node node = addConditionWaiter();
    // The saved state is used as a parameter. If it fails, an IllegalMonitorStateException is thrown
	int savedState = fullyRelease(node);
	boolean interrupted = false;
    // Block until there is a signal
	while (!isOnSyncQueue(node)) {
		LockSupport.park(this);
        if (Thread.interrupted())
			interrupted = true;
	}
    // Get the parameter of queue: get the parameter of queue that can not be saved exclusively, and take it as the parameter of queue that can not be saved exclusively
	if (acquireQueued(node, savedState) || interrupted)
		selfInterrupt();
}

2021 latest complete interview questions and answers (all sorted into documents). There are many dry goods, including mysql, netty, spring, threads, spring cloud, JVM, source code, algorithms, detailed learning plan and other materials. If you need to obtain these contents, please add Q sample: 222578377

Update continuously every day, for attention and three consecutive~

Topics: Java jvm Spring Boot Back-end Multithreading