Thread safety and deadlock

What is thread safety

In high-concurrency scenarios, the execution results of threads are certain and meet expectations; conversely, thread insecurity may lead to inconsistent results.

What is a thread-safe class

When multiple threads access a class, the class behaves correctly regardless of the scheduling method used by the runtime environment or how the threads will alternate execution, and without any additional synchronization or coordination in the calling code. , then this class is said to be thread-safe.

How to implement thread-safe classes

  • Thread closure: Encapsulate the object into a thread, so that only one thread can see the object. Even if the object is not thread-safe, it can still be thread-safe, because in a single-threaded environment, there is no concurrency Competition issues.
  • Stack closure: Local variables created in a thread are thread-safe.
  • ThreadLocal: ThreadLocal is implemented using a Map. The key corresponds to the thread and the value corresponds to the cached object. Each thread has an independent value object, achieving an effect similar to thread closure. Note: ThreadLocal itself is Thread-safe, but external logic may perform thread-unsafe operations on cached objects.
  • Stateless class: A class without attributes is called a stateless class. This class can only have member methods, achieving an effect similar to stack closure.
  • Immutable class: All member variables are added with the final keyword and cannot be modified, so there is no problem of concurrent writing. Note: If the member variable is an object, final can only guarantee that the reference to the object cannot be changed, and Object member properties may still have concurrency issues.
  • Lock or CAS: Add built-in lock or explicit lock or CAS to ensure the atomicity of code blocks or methods

Deadlock

Deadlock: Two or more threads hold each other’s locks and never release them. This phenomenon can be called a deadlock.

Academic definition:

  1. Mutually exclusive conditions
  2. Request and hold conditions
  3. no deprivation conditions
  4. loop wait condition

Handwritten deadlock code:

/**
 * @Author kk
 * @Date 2023-10-14
 * Handwritten deadlock example
 */
public class DeadLockDemo {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void playA() {
        synchronized (lock1) {
            System.out.println(ThreadTools.getThreadName() + "got lock1");
            SleepTools.second(1);
            synchronized (lock2) {
                System.out.println(ThreadTools.getThreadName() + "got lock2");
            }
        }
    }

    public static void playB() {
        synchronized (lock2) {
            System.out.println(ThreadTools.getThreadName() + "got lock2");
            SleepTools.second(1);
            synchronized (lock1) {
                System.out.println(ThreadTools.getThreadName() + "got lock1");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(DeadLockDemo::playA).start();
        playB();
    }
}

Monitor deadlocks

  • jdk command: jstack pid
  • arthas command: thread -b

To solve the deadlock:

The solution to deadlock is to overturn the deadlock principle mentioned earlier.

  • The order of acquiring multiple locks must be consistent
  • Release the lock after failure to acquire the lock

Example of orderly lock occupation:

/**
 * @Author kk
 * @Date 2023-10-14
 * Example of breaking deadlock - orderly lock occupation
 */
public class DeadLockBreakDemo1 {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    private void playA() {
        synchronized (lock1) {
            System.out.println(ThreadTools.getThreadName() + "got lock1");
            SleepTools.second(1);
            synchronized (lock2) {
                System.out.println(ThreadTools.getThreadName() + "got lock2");
            }
        }
    }

    private void playB() {
        synchronized (lock1) {
            System.out.println(ThreadTools.getThreadName() + "got lock1");
            SleepTools.second(1);
            synchronized (lock2) {
                System.out.println(ThreadTools.getThreadName() + "got lock2");
            }
        }
    }

    public static void main(String[] args) {
        DeadLockBreakDemo1 demo = new DeadLockBreakDemo1();
        new Thread(demo::playA).start();
        demo.playB();
    }
}

Example of releasing the owned lock after failure to grab the lock:

/**
 * @Author kk
 * @Date 2023-10-14
 * Example of breaking deadlock - lock release after failed lock grabbing
 */
public class DeadLockBreakDemo2 {

    private static final Lock lock1 = new ReentrantLock();
    private static final Lock lock2 = new ReentrantLock();

    private static final Random R = new Random();

    public void playA() throws InterruptedException {
        while (true) {
            if (lock1.tryLock()) {
                try {
                    System.out.println(ThreadTools.getThreadName() + "got lock1");
                    if (lock2.tryLock()) {
                        System.out.println(ThreadTools.getThreadName() + "got lock2");
                        lock2.unlock();
                        System.out.println(ThreadTools.getThreadName() + "lock2 released");
                        break;
                    }
                } finally {
                    lock1.unlock();
                    System.out.println(ThreadTools.getThreadName() + "lock1" released);
                }
            }
            Thread.sleep(R.nextInt(10));
        }
    }

    public void playB() throws InterruptedException {
        while (true) {
            if (lock2.tryLock()) {
                try {
                    System.out.println(ThreadTools.getThreadName() + "got lock2");
                    if (lock1.tryLock()) {
                        System.out.println(ThreadTools.getThreadName() + "got lock1");
                        lock1.unlock();
                        System.out.println(ThreadTools.getThreadName() + "lock1" released);
                        break;
                    }
                } finally {
                    lock2.unlock();
                    System.out.println(ThreadTools.getThreadName() + "lock2 released");
                }
            }
            Thread.sleep(R.nextInt(50));
        }
    }

    public static void main(String[] args) throws Exception {
        DeadLockBreakDemo2 demo = new DeadLockBreakDemo2();
        new Thread(() -> {
            try {
                demo.playA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        demo.playB();
    }
}

Possible problems in breaking deadlocks

  • Dynamic sequential lock occupation: the locking code is in order, but the locking object can be changed by dynamically passing parameters, see the example below
  • Livelock: Multiple threads succeed in acquiring the first lock at the same time, but fail to acquire the second lock at the same time. The cycle repeats, and the thread is in the RUNNABLE state.

Example of dynamic sequential lock occupation:

/**
 * @Author kk
 * @Date 2023-10-14 3:53
 * Dynamic sequential deadlock example
 */
public class DeadLockTrendsDemo {

    private void playA(Object lock1, Object lock2) {
        synchronized (lock1) {
            System.out.println(ThreadTools.getThreadName() + "got lock1");
            SleepTools.second(1);
            synchronized (lock2) {
                System.out.println(ThreadTools.getThreadName() + "got lock2");
            }
        }
    }

    private void playB(Object lock1, Object lock2) {
        synchronized (lock1) {
            System.out.println(ThreadTools.getThreadName() + "got lock1");
            SleepTools.second(1);
            synchronized (lock2) {
                System.out.println(ThreadTools.getThreadName() + "got lock2");
            }
        }
    }

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        DeadLockTrendsDemo demo = new DeadLockTrendsDemo();
        new Thread(() -> demo.playA(lock1, lock2)).start();
        demo.playB(lock2, lock1);
    }
}

Solve the problem of dynamic sequential lock occupation: sort the hash of locked objects and acquire locks in order

/**
 * @Author kk
 * @Date 2023-10-14 3:53
 * Dynamic sequence deadlock - solved by locking after objects are manually sorted
 */
public class DeadLockTrendsFixDemo {

    private static final Object lock3 = new Object();

    private void playA(Object lock1, Object lock2) {
        List<Object> lockList = sortObj(lock1, lock2);
        if (lockList != null) {
            synchronized (lockList.get(0)) {
                System.out.println(ThreadTools.getThreadName() + "got lock1");
                SleepTools.second(1);
                synchronized (lockList.get(1)) {
                    System.out.println(ThreadTools.getThreadName() + "got lock2");
                }
            }
        } else {
            // hash equal, reference third-party lock
            synchronized (lock3) {
                synchronized (lock1) {
                    System.out.println(ThreadTools.getThreadName() + "got lock1");
                    SleepTools.second(1);
                    synchronized (lock2) {
                        System.out.println(ThreadTools.getThreadName() + "got lock2");
                    }
                }
            }
        }
    }

    // Sort by hash and save to the list in ascending order; if the hashes are equal, return null
    private List<Object> sortObj(Object lock1, Object lock2) {
        int hash1 = System.identityHashCode(lock1);
        int hash2 = System.identityHashCode(lock2);
        if (hash1 == hash2) {
            return null;
        } else if (hash1 > hash2) {
            return Arrays.asList(lock2, lock1);
        } else {
            return Arrays.asList(lock1, lock2);
        }
    }

    public static void main(String[] args) {
        Object lock1 = new Object();
        Object lock2 = new Object();
        DeadLockTrendsFixDemo demo = new DeadLockTrendsFixDemo();
        new Thread(() -> demo.playA(lock1, lock2)).start();
        demo.playA(lock2, lock1);
    }
}

Solve the livelock problem: After failing to acquire the lock, sleep random numbers to avoid competing for a lock at the same time. For the code, refer to DeadLockBreakDemo2 above

Thread starvation

The thread priority is low and the CPU time slice cannot be allocated for a long time.