[Lock Thought] Why is the default strategy of synchronized unfair?

  Hello everyone, I am Brother Coder. In today’s ever-changing technology, what we really should spend time learning are those unchanging programming ideas . So today let’s talk about the ideas of fair and unfair strategies. I saw a problem a few days ago.” Why synchronized is unfair ", I thought about it carefully and found that not only synchronized, but also the default strategy of ReentrantLock is unfair. Unfairness is a strategy for implementing locks, not only in Java, but also in other languages. It is unfair, so today we will talk in detail about " why the default lock implementation strategies in various languages ​​are unfair ". Is this unfairness really completely unfair random access?

If you don’t dislike it, you can pay attention to it. Thanks here:
Nuggets or WeChat search: todocoder
will continue to share programming ideas and server-side related content such as Java and Go.

fair and unfair

First of all, let's take a look at what is a fair lock and an unfair lock. In order to make it clearer for everyone, we use a legend to illustrate the fair and unfair scenarios.

Scenarios for fair locks

Let's take a look at the fair lock scenario first. As the name suggests, fair locks refer to the allocation of locks in the order in which threads request them; If you come first, you will get the lock first, then threads 2 and 3 will wait in the queue. After thread 1 releases the lock, threads 2 and 3 will acquire the lock in turn. If there is thread 4 competing for the lock at this time, it will be queued. Wait behind threads 2, 3.

Then after thread 1 releases the lock, threads 2, 3, and 4 will acquire the lock in turn. The reason why thread 2 acquires it first is that it waits the longest.

Scenarios for unfair locks

Unfair locks refer to allocations that are not in order. Under certain circumstances, queues can be jumped. The things to note here are:

The unfairness here is not completely random, nor is it that you can jump in the queue at will, but jump in the queue at the right time.

So what is the right time?  For example, when thread 1 finishes executing, threads 2, 3, and 4 are in the queue at this time. At this time, thread 5 comes to request a lock. Just as thread 1 releases the lock, the current lock will be given to thread 5 instead of thread 2. This is the so-called right time to jump in the queue , as shown in the figure:

Then after thread 5 finishes executing, if thread 6 happens to come over, then this lock will be given to thread 6, and if there is no such coincidence, it will be given to thread 2.

I know you have doubts. According to this logic, if it happens that thread 7, thread 8, thread 9..., then thread 2 will wait forever. Yes, this is the thread starvation caused by unfair locks.  This is also the shortcoming of unfair locks, and another article will be published later to analyze the solution to thread starvation, pay attention not to get lost

Seeing this, you may be even more confused. Unfair locks have the disadvantage of causing starvation, so why are almost all default lock mechanisms at the language level unfair strategies? Is our time queuing up all wasted? Standing in line for a long time and being cut in line by others? This leads to the question at the beginning of our article: Why are the default policies of lock implementations in various languages ​​​​unfair?

Why are the default policies of lock implementations in various languages ​​unfair?

There is a reason for using an unfair strategy. For example, thread 1 holds a lock. At this time, threads 2, 3, and 4 request to come in one by one, then they queue up in the queue one by one, fall into waiting, that is, enter the blocked state, and then the thread 1 is executed, it should be thread 2's turn to wake up and acquire the lock, but at this time it happens that thread 5 requests the lock, then according to the principle of unfairness, thread 5 acquires the lock, because waking up thread 2 will have a lot of trouble. Large overhead, because most of the execution of the program is very fast, it is likely that thread 5 has already completed execution before waking up thread 2, so according to the logic of the unfair strategy, thread 5 will be allowed to acquire the lock first, compared to Because of the long process of waiting for thread 2 to wake up, it will be more efficient to directly execute thread 5, which is a win-win situation.

There are many benefits based on the above scenario

For thread 5 : It does not need any waiting to directly acquire the lock and execute it, which improves its efficiency.

For thread 2 : the time it acquires the lock is not delayed, because when it is woken up, thread 5 has already released the lock, because the execution speed of thread 5 is faster than the wake-up speed of thread 2 .

Therefore, under normal circumstances, the default lock strategy is an unfair strategy, which is to improve the overall operating efficiency.

Pros and Cons of Fairness and Unfairness

We next look at the pros and cons of fairness and unfairness, as shown in the table.

The advantage of a fair lock is that each thread is fair and equal. After waiting for a period of time, each thread has a chance to execute, but its disadvantage is that the overall execution speed is slower and the throughput is smaller. The advantage of an unfair lock is that the overall execution The speed is faster and the throughput is greater, but at the same time, there may be a thread starvation problem, that is to say, if there are always threads jumping in the queue, the threads in the waiting queue may not be run for a long time.

Combined with source code analysis

Combined with the source code of ReentrantLock, let's analyze how fair and unfair locks are implemented. Looking at the source code, we can see that the ReentrantLock class contains a Sync class, which inherits from AQS (AbstractQueuedSynchronizer). The code is as follows:

 
 

java

copy code

public class ReentrantLock implements Lock { private final Sync sync; // Sync 类的代码: abstract static class Sync extends AbstractQueuedSynchronizer {...} ... // 非公平锁 static final class NonfairSync extends Sync {...} // 公平锁 static final class FairSync extends Sync {...} }

According to the code, Sync has two subclasses: fair lock FairSync and unfair lock NonfairSync. Let's take a look at the source code of the fair lock and unfair lock locking methods.

The lock acquisition source code of the fair lock is as follows:

 
 

java

copy code

protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //这里判断了 hasQueuedPredecessors(), // 判断在等待队列中是否已经有线程在排队了 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 这里是可重入锁特性的实现,比较简单易懂,这里不做过多探讨,有兴趣可自行查阅源码。 ... return true; } return false; }

Unfair lock acquisition lock source code is as follows:

 
 

java

copy code

final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //这里没有判断 hasQueuedPredecessors(), // 也就是直接自旋修改状态,能改就插队了 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { ... return true; } return false; }

Comparing the above codes, it can be clearly seen that the only difference between fair locks and unfair locks is that fair locks have an additional restriction when acquiring locks: it is to determine whether there are already threads hasQueuedPredecessors() in the waiting queue. This is the core difference between a fair lock and an unfair lock. If it is a fair lock, once a thread is already queued, the current thread will no longer try to acquire the lock; Try to acquire the lock, if you can't get it, go to the queue again.

**Note:** In the fair lock, there is a special case that we need to pay attention to, the tryLock() method, which does not abide by the set fairness principle.

When a thread executes the tryLock() method, it will try to see if it can jump in the queue, which is a scheduling mechanism similar to unfair locks. We can take a look at the source code:

 
 

java

copy code

public boolean tryLock() { return sync.nonfairTryAcquire(1); }

What is called here is nonfairTryAcquire(), which indicates that it is unfair and has nothing to do with whether the lock itself is a fair lock.

at last

To sum up, fair locks acquire locks in the order in which multiple threads apply for locks, so as to achieve fairness. Unfair locks do not consider the queuing situation when locking, and directly try to acquire the lock. In this way, there will be a situation where the lock is obtained first by jumping in the queue, but this also improves the overall efficiency, with greater throughput and faster execution.

Guess you like

Origin blog.csdn.net/BASK2312/article/details/131305649