[JUC] 4. Investigation of reentrant locks, fair locks, unfair locks and deadlocks

Article directory

  • 1. Fair lock and unfair lock
  • 2. Reentrant lock
  • 3. Deadlock
  • 4. Verification and troubleshooting of deadlocks

1. Fair lock and unfair lock

Still the previous example of selling 30 tickets in three threads:

//Resource class
class LTicket{<!-- -->

    private Integer number = 30;

    private final ReentrantLock lock = new ReentrantLock();

    public void sale(){<!-- -->
        //Lock
        lock.lock();
        try {<!-- -->
            if( number > 0 ){<!-- -->
                System.out.println(Thread.currentThread().getName() + ": Sold tickets, remaining" + number--);
            }

        } finally {<!-- -->
            //Release the lock and write it in the finally statement to prevent the above exception from causing the lock to not be released.
            lock.unlock();
        }
    }
}

</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack. png" alt="" title="">

Start three threads and call the resource class method:

public class LSaleTicket {<!-- -->

    public static void main(String[] args) {<!-- -->
        LTicket ticket = new LTicket();
        new Thread(() -> {<!-- -->
            for(int i = 0; i < 40; i + + ){<!-- -->
                ticket.sale();
            }
        },"AA").start();

        new Thread(new Runnable() {<!-- -->
            @Override
            public void run() {<!-- -->
                for(int i = 0; i < 40; i + + ){<!-- -->
                    ticket.sale();
                }
            }
        },"BB").start();
        new Thread(() -> {<!-- -->
            for (int i = 0; i < 40; i + + ) {<!-- -->
                ticket.sale();
            }
        }, "CC").start();
    }
}


</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack. png" alt="" title="">

When executed, it will be found that AA often sells out all the tickets in one thread:

This is an unfair lock. When using the new Lock object above:

private final ReentrantLock lock = new ReentrantLock();
//No parameters are passed, the default is unfair lock

Source code: three eyes, true is fair

Change to fair lock:

private final ReentrantLock lock = new ReentrantLock(true);

Take a look at the effect:

Summarize:

  • Unfair lock: may cause thread starvation, but is highly efficient
  • Fair lock: The sun shines brightly, but the efficiency is lower than unfair lock

Comparing the two, it’s like going to the library for self-study. The unfair lock is to sit down when you see the seat, while the fair lock is to ask if there is anyone here first. If there is, then queue up. The source code of the fair lock:


Q: Why are there designs for fair locks and unfair locks, and why are they unfair by default?

A: Unfair locks can make fuller use of CPU time slices and reduce CPU idle time. And the cost of CPU thread switching is also reduced (originally one thread A can complete the task, but if it is equally divided into three threads ABC, it has to switch back and forth. This is synchronization, and there is no talk about multi-thread concurrency improving efficiency), so the overall unfair lock Performance is better than fair lock.

Q: When to use fair locks and when to use unfair locks?

If you want higher throughput, it is obvious that unfair lock is more appropriate, because it saves a lot of thread switching time, and the throughput will naturally increase. Otherwise, use fair lock and everyone can use it fairly.

2. Reentrant lock

Both synchronized (implicit) and Lock (explicit) are reentrant locks. The explicit and implicit here refer to the Lock that requires developers to manually lock and unlock. Re-entrant locks mean that there is a lock on your home door, a lock on your bedroom door, and a lock on your bathroom. But as long as you unlock the door, you don’t need to unlock the bedroom and bathroom again, and you can enter these rooms freely. . By analogy to the code, they use the same lock. Reentrant locks are also called recursive locks.

Reentrant lock means that when the same thread acquires the lock in the outer method, the inner method that enters the thread will automatically acquire the lock (provided that the lock object must be the same object ), it will not be blocked because it has been obtained before but not released.

If it is a recursive calling method with synchronized modification, wouldn’t it be a big joke if the program is blocked by itself the second time it enters, and it is trapped in a cocoon. Therefore, ReentrantLock and synchronized in Java are both reentrant locks. One advantage of reentrant locks is that they can avoid deadlocks to a certain extent.


The implementation of reentrant locks, the bottom layer is the __count attribute in Monitor

Write a synchronized synchronization code block:

public class SyncLockDemo {<!-- -->
    
    public static void main(String[] args) {<!-- -->
        
        Object o = new Object();
        new Thread(() -> {<!-- -->
            synchronized (o){<!-- -->
                System.out.println(Thread.currentThread().getName() + "outer");
                synchronized (o){<!-- -->
                    System.out.println(Thread.currentThread().getName() + "middle layer");
                    synchronized (o){<!-- -->
                        System.out.println(Thread.currentThread().getName() + "inner layer");
                    }
                }
            }
        },"t1").start();

    }
}

</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack. png" alt="" title="">

About reentrant locks, also called recursive locks:

public class SyncLockDemo {<!-- -->
    
    public synchronized void add(){<!-- -->
    //I don’t want to write a synchronization method m1, and then the m1 method body calls the synchronization method m2, and then m2 calls the synchronization method m3, and recurses directly.
        add();
    }

    public static void main(String[] args) {<!-- -->
        
        new SyncLockDemo().add();
     
    }
}

You can see that when the synchronized instance method add is called recursively, a StackOverflowError will appear, which illustrates the characteristics of reentrant locks. If it is not reentrant, then if the add method is called during recursion, there will be no object lock for it to use. Then use lock to display the demo:

In addition, lock and unlock must be paired. Of course, if the unlock of the inner lock is commented out here, it can still run successfully. After entering the gate, you can move freely, but you lack unlock, and the thread will be blocked later. If you want to lock, you won’t be able to wait for it to be locked. It’s equivalent to entering the door, having a rest, then coming out but closing the door without the key.

3. Deadlock


When the t1 thread executes a certain synchronization code block, it uses the lock of object 1 and the lock of object 2. That is, the t1 thread needs to lock object 1 first, and then lock object 2. After all locks, the execution of the synchronization code block is completed, and then the two are released at once. Object lock. (A lock-B lock-B unlock-A unlock)

When the t2 thread executes another synchronized code block, it needs to lock object 2 first, and then lock object 1 before the execution of this synchronized code block is completed, and then releases the two object locks. (B lock-A lock-A unlock-B unlock)

Like this: when t1 locks object 2, it finds that it is locked, and then waits. On the other side: when t2 locks object 1, it finds that object 1 has been locked, and the two threads fall into endless waiting at the same time… It’s awkward. . At this time, if there is no external interference, the execution will not continue. The execution result is that the cursor flashes and there is no output, but the execution does not end.

Causes of deadlock:
  • Insufficient system resources
  • The process running sequence is unreasonable
  • Improper allocation of resources

Write a deadlock demo:

public class DeadLock {<!-- -->

    public static void main(String[] args) {<!-- -->
        Object o1= new Object();
        Object o2= new Object();

        new Thread(() -> {<!-- -->
            synchronized (o1){<!-- -->
                System.out.println(Thread.currentThread().getName() + "===>Holding the o1 object lock and trying to obtain the o2 object lock");
                try {<!-- -->
                    Thread.sleep(200); //Sleep for a while while holding the o1 lock. Don't release the two locks too soon after execution to ensure that deadlock will occur.
                } catch (InterruptedException e) {<!-- -->
                    e.printStackTrace();
                }
                synchronized (o2){<!-- -->
                    System.out.println(Thread.currentThread().getName() + "===>Obtained o2 object lock");
                }
            }
        },"t1").start();

        new Thread(() -> {<!-- -->
            synchronized (o2){<!-- -->
                System.out.println(Thread.currentThread().getName() + "===>Holding the o2 object lock and trying to obtain the o1 object lock");
                try {<!-- -->
                    Thread.sleep(200);
                } catch (InterruptedException e) {<!-- -->
                    e.printStackTrace();
                }
                synchronized (o1){<!-- -->
                    System.out.println(Thread.currentThread().getName() + "===>Obtained o1 object lock");
                }
            }
        },"t2").start();
    }

}

</code><img class="look-more-preCode contentImg-no-view" src="//i2.wp.com/csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreBlack. png" alt="" title="">

run:

4. Verification and troubleshooting of deadlock

Of course, you cannot say it is a deadlock as soon as you see no exit0 and no output. This may also happen with infinite loops and remote calls. To verify whether it is a deadlock:

Method 1: View stack information

  • jps: similar to Linux ps -ef
  • jstack: JVM comes with stack tracing tool

About jps:

There is no environment variable configured. In the IDEA terminal, cd here first and then execute:

Then continue to execute jstack in the IDEA terminal and view the stack information:

jstack 10212

Key information returned:

Method 2: Open the Java console with graphical jconsole

cmd input jconsole:


Regarding deadlock and thread safety optimization:

Synchronized will reduce program execution efficiency, reduce system throughput, and worsen user experience. To solve thread safety, consider:

  • Use local variables instead of instance variables and static variables
  • If you must use instance variables, consider creating a few more objects. If they are shared with other objects, there will be no security issues
  • If neither of the above two is possible, use synchronized