When developers use concurrency in their applications to improve performance, developers need to be aware that threads can block each other. When the entire application executes slower than expected, the application does not execute as expected execution time. In this chapter, we need to carefully analyze the active issues that may affect the application of multithreading.
Dead lock
The concept of deadlocks is well known among software developers, and even ordinary computer users often use this concept, although not in the right circumstances. Strictly speaking, a deadlock means that two or more threads are waiting for another thread to release its locked resource, while the thread that requests the resource also locks the resource requested by the other thread itself. As follows:
1for2for resource A
For a better understanding of the problem, refer to the following code:
Public classDeadlock implements Runnable {Private Static FinalObject Resource1 =NewObject ();Private Static FinalObject Resource2 =NewObject ();Private FinalRandom random =NewRandom (System.currenttimemillis ()); Public Static voidMain (string[] args) {Thread myThread1 =NewThread (NewDeadlock (),"Thread-1"); Thread myThread2 =NewThread (NewDeadlock (),"Thread-2"); Mythread1.start (); Mythread2.start (); } Public voidRun () { for(inti =0; I <10000; i++) {Boolean b = Random.nextboolean ();if(b) {System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Trying to lock resource 1. ");synchronized(Resource1) {System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Locked resource 1. "); System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Trying to lock resource 2. ");synchronized(RESOURCE2) {System. out. println ("["+ Thread. CurrentThread (). GetName () +"] Locked resource 2. "); } } }Else{System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Trying to lock resource 2. ");synchronized(RESOURCE2) {System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Locked resource 2. "); System. out. println ("["+ Thread.CurrentThread (). GetName () +"] Trying to lock resource 1. ");synchronized(Resource1) {System. out. println ("["+ Thread. CurrentThread (). GetName () +"] Locked resource 1. "); } } } } }}
As you can see from the code above, two threads are started separately and try to lock 2 static resources. But for deadlocks, we need two threads to lock resources in a different order, so we use random instances to select the resources that the thread wants to lock first.
If the Boolean variable b
is true
, it locks and resource1
then tries to get resource2
the lock. If b
it is false
, the thread locks first resource2
, but attempts to lock resource1
. The program will encounter a deadlock problem in a moment, and then it will hang until we end the JVM:
[Thread-1] Trying toLockResource1.[thread-1] Locked Resource1.[thread-1] Trying to LockResource2.[thread-1] Locked Resource2.[thread-2] Trying to LockResource1.[thread-2] Locked Resource1.[thread-1] Trying to LockResource2.[thread-1] Locked Resource2.[thread-2] Trying to LockResource2.[thread-1] Trying to LockResource1.
In the above execution, holds the lock, waits for the lock, thread-1
resource2
while the thread holds the lock resource1
thread-2
resource1
, waiting resource2
for the lock.
If we b
configure the value true
or, we will false
not encounter deadlocks, because the order of execution is always consistent, and the order of thread-1
the thread-2
request lock is always consistent. Two threads will request the lock in the same order, so a maximum of one thread will be temporarily blocked and eventually executed sequentially.
Presumably, the following conditions are required to cause a deadlock:
- Mutex : A resource must exist at some point and can only be accessed by a single thread.
- Resource hold : When a resource is locked, the thread still needs to get a lock on another resource.
- There is no preemption policy : When a thread has been holding a resource for some time, there is no mechanism to seize the thread to lock the resource.
- loop wait : At run time there must be two or more threads that request each other's locked resources.
Deadlocks are more common in multi-threaded applications, although the conditions for deadlocks may appear to be more significant. Developers can avoid deadlocks by breaking the necessary conditions for deadlock formation, as follows:
- Mutual exclusion: This requirement is usually unavoidable, and resources are often only mutually exclusive access. But that's not always the case. When using a DBMS system, it is possible to replace the original pessimistic locking mechanism with a similar optimistic lock (lock a row in the table when updating the data).
- There is also a viable option, which is to handle the resource holding, when the lock of a resource is acquired, the lock of other necessary resources is acquired immediately, and if the lock fails, all the previously mutex resources are freed. Of course, this is not always possible, and the resources that might be locked are not known before, or are discarded.
- If the lock is not immediately available, one way to prevent a deadlock is to configure the lock for the last time-out. A
ReentrantLock
similar time-out method is provided in the SDK class.
- From the code above, we can see that if the order of locked resources for each thread is the same, no deadlock will occur. This process can be achieved by abstracting all the code that requests the lock into a single method, which is then called by the thread. This can effectively avoid deadlocks.
In a more advanced application, developers may want to consider implementing a system that detects deadlocks. In this system, some thread-based monitoring is implemented, and logs are logged when the future acquires a lock and attempts to request another lock. If a graph is formed with threads and locks, the developer is able to detect that 2 different threads hold resources and request additional blocking resources at the same time. If the developer can detect and be able to force the blocked thread to release the acquired resources, it can automatically detect deadlocks and automatically fix the deadlock problem.
Hunger
The thread scheduler determines which RUNNABLE
threads in the state will execute in the order they are executed. Decisions are generally thread-based, so low-priority threads get less CPU time, and high-priority threads get more CPU time. Of course, this kind of dispatching sounds more reasonable, but sometimes it can cause problems. If a high-priority thread is always executed, then the low-priority thread will not be able to get enough time to execute and be in a state of starvation. Therefore, it is recommended that developers only configure thread priority when it is really necessary.
An example of a very complex thread of starvation is the finalize()
method. This feature in the Java language can be used for garbage collection, but when a developer looks at finalizer
the priority of a thread, it will find that it is not running at the highest priority level. Therefore, it is possible that the finalize()
method will perform longer than other methods.
Another problem with the execution time is that the thread is not defined in what order the code block is synchronized. When many parallel threads need to pass through the encapsulated synchronization code block, there are threads that wait longer than other threads to get into the synchronization code faster. In theory, they may never be able to enter blocks of code. This problem can be solved by using a fair locking scheme. A fair lock takes into account the thread's wait time when selecting the next thread. One of the implementations of a fair lock is java.util.concurrent.locks.ReentrantLock
:
If you use ReentrantLock
the following constructor:
/** * Creates an instance of {@code ReentrantLock} with the * given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */ publicReentrantLock(boolean fair) { newnew NonfairSync(); }
Incoming true
, then ReentrantLock
a fair lock, which allows the thread to get the lock executed sequentially in the pending order. This can reduce the starvation of threads, but it does not completely solve the problem of starvation, after all, the scheduling of threads is dispatched by the operating system. Therefore, the ReentrantLock
class only considers the thread that waits for the lock, and the dispatch does not work. For example, although a fair lock is used, the operating system gives low-priority threads a short execution time.
Java Threading and Multithreading (15)--Thread activity