[High Concurrency] Interviewer: Synchronized is provided in Java, why do we need to provide Lock?

Write in front

The synchronized keyword is provided in Java to ensure that only one thread can access the synchronized code block. Now that the synchronized keyword has been provided, why is the Lock interface still provided in the Java SDK package? Is this reinventing the wheel and making it unnecessary? Today, we will discuss this issue together.

Reinvent the wheel?

Since the synchronized keyword is provided in the JVM to ensure that only one thread can access the synchronized code block, why provide the Lock interface? Is this reinventing the wheel? Why did Java designers do this? Let us look down with questions.

Why provide Lock interface?

Many friends may have heard that in Java 1.5 version, synchronized performance is not as good as Lock, but after Java 1.6 version, synchronized has made a lot of optimizations and performance has improved a lot. Now that the performance of the synchronized keyword has been improved, why use Lock?

Insert picture description here

If we think in a deeper level, it is not difficult to think: we cannot actively release the lock using synchronized locking, which will involve the problem of deadlock.

Deadlock problem

If a deadlock is to occur, the following four necessary conditions must exist, and one of the four is indispensable.

Insert picture description here

  • Mutually exclusive conditions

A certain resource is occupied by only one thread in a period of time. If other threads request the resource at this time, the requesting thread can only wait.

  • Inalienable condition

The resource obtained by a thread cannot be forcibly taken away by other threads before it is used up, that is, it can only be released by the thread that obtained the resource (only active release).

  • Request and hold conditions

The thread has reserved at least one resource, but has made a new resource request, and the resource is already occupied by other threads, the requesting thread is blocked at this time, but it keeps holding on to the resources it has obtained.

  • Loop waiting condition

When a deadlock occurs, there must be a process waiting queue {P1,P2,...,Pn}, where P1 waits for the resources occupied by P2, P2 waits for the resources occupied by P3, ..., Pn waits for the resources occupied by P1, forming a process waiting loop Road, the resources occupied by each process in the loop are simultaneously applied for by another process, that is, the resources occupied by the next process affectionately by the previous process.

The limitations of synchronized

If a deadlock occurs in our program using the synchronized keyword, the key to synchronized is that it cannot destroy the "inalienable" deadlock condition. This is because when synchronized applies for resources, if the application fails, the thread directly enters the blocking state, and the thread enters the blocking state, nothing can be done, and the resources already occupied by the thread cannot be released.

However, in most scenarios, we all hope that the condition of "inalienability" can be destroyed. That is to say, for the condition of "inalienability", when a thread that occupies some resources further applies for other resources, if it fails to apply for it, it can actively release the resources it occupies, so that the condition of inalienability is destroyed.

If we redesign the lock ourselves to solve the synchronized problem, how should we design it?

Solve the problem

After understanding the limitations of synchronized, if we are to implement a synchronized lock ourselves, how should we design it? That is to say, when we design the lock, how do we solve the limitation of synchronized? Here, I think this problem can be considered from three aspects.
Insert picture description here

(1) Able to respond to interrupts. The problem with synchronized is that after holding lock A, if the attempt to acquire lock B fails, the thread enters the blocking state. Once a deadlock occurs, there is no chance to wake up the blocked thread. But if the blocked thread can respond to the interrupt signal, that is, when we send the interrupt signal to the blocked thread, we can wake it up, then it has the opportunity to release the lock A it once held. This destroys the condition of inalienability.

(2) Support timeout. If the thread does not acquire the lock within a period of time, instead of entering the blocking state, but returning an error, then this thread also has a chance to release the lock it once held. This can also destroy the inalienable condition.

(3) Acquire the lock without blocking. If the attempt to acquire the lock fails and it does not enter the blocking state, but returns directly, then this thread also has the opportunity to release the lock it once held. This can also destroy the inalienable condition.

Reflected on the Lock interface, they are the three methods provided by the Lock interface, as shown below.

// 支持中断的API
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的API
boolean tryLock();
  • lockInterruptibly()

Support interruption.

  • tryLock() method

The tryLock() method has a return value, which means that it is used to try 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, which means that this method is no matter what Will return immediately. When you can't get the lock, you won't be there forever.

  • tryLock(long time, TimeUnit unit)方法

The tryLock(long time, TimeUnit unit) method is similar to the tryLock() method, but the difference is that this method will wait for a certain time when the lock is not available, and if the lock is not available within the time limit, it will return false. If the lock is acquired at the beginning or during the waiting period, it returns true.

In other words, for the deadlock problem, Lock can destroy the inalienable condition. For example, our following program code destroys the inalienable condition of the deadlock.

public class TansferAccount{
    
    
    private Lock thisLock = new ReentrantLock();
    private Lock targetLock = new ReentrantLock();
    //账户的余额
    private Integer balance;
    //转账操作
    public void transfer(TansferAccount target, Integer transferMoney){
    
    
        boolean isThisLock = thisLock.tryLock();
        if(isThisLock){
    
    
            try{
    
    
                boolean isTargetLock = targetLock.tryLock();
                if(isTargetLock){
    
    
                    try{
    
    
                         if(this.balance >= transferMoney){
    
    
                            this.balance -= transferMoney;
                            target.balance += transferMoney;
                        }   
                    }finally{
    
    
                        targetLock.unlock
                    }
                }
            }finally{
    
    
                thisLock.unlock();
            }
        }
    }
}

Exception, there is a ReentrantLock under Lock, and ReentrantLock supports fair lock and unfair lock.

When using ReentrantLock, there are two constructors in ReentrantLock, one is a parameterless constructor, and the other is a constructor with fair parameters. The fair parameter represents the fairness strategy of the lock. If true is passed in, it means that a fair lock needs to be constructed; otherwise, it means that an unfair lock must be constructed. As shown in the following code snippet.

//无参构造函数: 默认非公平锁
public ReentrantLock() {
    
    
	sync = new NonfairSync();
} 
//根据公平策略参数创建锁
public ReentrantLock(boolean fair){
    
    
	sync = fair ? new FairSync() : new NonfairSync();
}

The lock implementation essentially corresponds to an entry waiting queue. If a thread does not acquire the lock, it will enter the waiting queue. When a thread releases the lock, it needs to wake up a waiting thread from the waiting queue. If it is a fair lock, the wake-up strategy is to wake up whoever waits for a long time, which is fair; if it is an unfair lock, this fairness guarantee is not provided, and threads with a short waiting time may be awakened first. While Lock supports fair locks, synchronized does not support fair locks.

Finally, it is worth noting that when using Lock to lock, you must release the lock in the finally{} code block, for example, as shown in the following code snippet.

try{
    
    
    lock.lock();
}finally{
    
    
    lock.unlock();
}

Note: For other detailed descriptions of synchronized and Lock, please refer to it by yourself.

Well, let's talk about it today! Don't forget to like it, give it to someone who is watching and forward it, so that more people can see, learn together and make progress together! !

Guess you like

Origin blog.csdn.net/l1028386804/article/details/108613319