A deadlock means that two or more actions are always waiting for other actions to complete, so that all actions are always blocked. It is very difficult to detect deadlocks in the development phase, and to remove deadlocks, you often need to restart the program. Worse, deadlocks usually occur in the production process with the heaviest load, and it is very difficult to find them during testing. The reason for this is that all possible intersections between test threads are unrealistic. Although some static analysis libraries can help us find possible deadlocks, it is necessary to detect deadlocks at runtime and obtain useful information, so that we can solve this problem, restart the program, or do something else.
Use the ThreadMXBean class in programming to detect deadlocks
Java 5 introduces the ThreadMXBean interface, which provides a variety of methods to monitor threads. I recommend that you understand all these methods, because they provide you with a lot of useful operations when you are not using external tools, so that you can monitor program performance. Here, the method we are interested in is findMonitorDeadlockedThreads. If you have used Java 6, the corresponding method is findDeadlockedThreads. The difference between the two is that findDeadlockedThreads can also detect the deadlock caused by the owner locks (java. util. concurrent), while findMonitorDeadlockedThreads can only detect monitor locks (for example, synchronous blocks ). Since the old version is retained only for compatibility considerations, I will use the new version. Here, the idea of programming is to encapsulate the periodic detection of deadlocks into a reusable component, and then we only need to start it and follow it.
One way to implement scheduling is through the executor framework, that is, a group of well-abstracted and easy-to-use multi-threaded classes.
The code is as follows: |
Copy code |
ScheduledExecutorService schedors = Executors. newScheduledThreadPool (1 ); This. Schedrate. scheduleAtFixedRate (deadlockCheck, period, period, unit ); |
It is that simple. After we set a specific time by selecting the cycle and time unit, we get a thread for periodic calls. Next, we want to extend the functionality so that users can provide the behavior triggered when the program detects a deadlock. Finally, we need a method to receive a series of objects used to describe all threads in a deadlock.
The code is as follows: |
Copy code |
Void handleDeadlock (final ThreadInfo [] deadlockedThreads ); |
Now, the deadlock detection class is ready.
The code is as follows: |
Copy code |
Public interface DeadlockHandler { Void handleDeadlock (final ThreadInfo [] deadlockedThreads ); } Public class DeadlockDetector { Private final DeadlockHandler deadlockHandler; Private final long period; Private final TimeUnit unit; Private final ThreadMXBean mbean = ManagementFactory. getThreadMXBean (); Private final ScheduledExecutorService scheduler = Executors. newScheduledThreadPool (1 ); Final Runnable deadlockCheck = new Runnable (){ @ Override Public void run (){ Long [] deadlockedThreadIds = DeadlockDetector. this. mbean. findDeadlockedThreads (); If (deadlockedThreadIds! = Null ){ ThreadInfo [] threadInfos = DeadlockDetector. this. mbean. getThreadInfo (deadlockedThreadIds ); DeadlockDetector. this. deadlockHandler. handleDeadlock (threadInfos ); } } }; Public DeadlockDetector (final DeadlockHandler deadlockHandler, Final long period, final TimeUnit unit ){ This. deadlockHandler = deadlockHandler; This. period = period; This. unit = unit; } Public void start (){ This. Schedrate. scheduleAtFixedRate ( This. deadlockCheck, this. period, this. period, this. unit ); } }
|
Let's try it. First, we need to create a handler to output the information of the deadlock thread to System. err. In actual scenarios, we can use it to send emails, for example:
The code is as follows: |
Copy code |
Public class DeadlockConsoleHandler implements DeadlockHandler { @ Override Public void handleDeadlock (final ThreadInfo [] deadlockedThreads ){ If (deadlockedThreads! = Null ){ System. err. println ("Deadlock detected! "); Map <Thread, StackTraceElement []> stackTraceMap = Thread. getAllStackTraces (); For (ThreadInfo threadInfo: deadlockedThreads ){ If (threadInfo! = Null ){ For (Thread thread: Thread. getAllStackTraces (). keySet ()){ If (thread. getId () = threadInfo. getThreadId ()){ System. err. println (threadInfo. toString (). trim ()); For (StackTraceElement ste: thread. getStackTrace ()){ System. err. println ("t" + ste. toString (). trim ()); } } } } } } } } |
This process repeats in all stack traces and prints the corresponding stack trace for each thread information. In this way, we can accurately know the waiting position and object of each thread. But this method has a defect-when a thread is only waiting for a while, it may be treated as a temporary deadlock, which leads to an error alarm. Because of this, when we handle deadlocks, the original thread cannot continue to exist, and the findDeadlockedThreads method will return no such thread. To avoid possible NullPointerException, we need to be cautious about this situation. Finally, let's create a deadlock to see how the system runs.
The code is as follows: |
Copy code |
DeadlockDetector deadlockDetector = new DeadlockDetector (new DeadlockConsoleHandler (), 5, TimeUnit. SECONDS ); DeadlockDetector. start (); Final Object lock1 = new Object (); Final Object lock2 = new Object (); Thread thread1 = new Thread (new Runnable (){ @ Override Public void run (){ Synchronized (lock1 ){ System. out. println ("Thread1 acquired lock1 "); Try { TimeUnit. MILLISECONDS. sleep (500 ); } Catch (InterruptedException ignore ){ } Synchronized (lock2 ){ System. out. println ("Thread1 acquired lock2 "); } } } }); Thread1.start (); Thread thread2 = new Thread (new Runnable (){ @ Override Public void run (){ Synchronized (lock2 ){ System. out. println ("Thread2 acquired lock2 "); Synchronized (lock1 ){ System. out. println ("Thread2 acquired lock1 "); } } } }); Thread2.start (); |
Output:
The code is as follows: |
Copy code |
Thread1 acquired lock1 Thread2 acquired lock2 Deadlock detected! "Thread-1" Id = 11 BLOCKED on java. lang. Object @ 68ab95e6 owned by "Thread-0" Id = 10 Deadlock. DeadlockTester $ 2.run( DeadlockTester. java: 42) Java. lang. Thread. run (threads. java: 662) "Thread-0" Id = 10 BLOCKED on java. lang. Object @ 58fe64b9 owned by "Thread-1" Id = 11 Deadlock. DeadlockTester $ 1.run( DeadlockTester. java: 28) Java. lang. Thread. run (threads. java: 662) |
Remember, the deadlock detection overhead may be very high. You need to use your program to test whether you really need the deadlock detection and how often it takes to detect the deadlock. I suggest that the deadlock detection interval be at least several minutes, because more frequent detection does not make much sense, because we do not have a recovery plan, all we can do is debug and handle errors or restart the program and pray that no deadlock will occur again.