import java.lang.reflect.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.Consumer;
import sun.misc.Unsafe;

class Loader extends URLClassLoader {
    TimingHelper timing;
    Object lock;

    Loader(Object lock, URL[] urls, TimingHelper timing) {
        super(urls, null);
        this.timing = timing;
        this.lock = lock;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name.equals("Helper2")) {
            timing.on(1);
            timing.on(4);
        }
        return super.findClass(name);
    }

    @Override
    protected Object getClassLoadingLock(String className) {
        // to remove deadlock
        return lock;
    }
}

class TimingHelper {
    private int s;
    private Timer t = new Timer(true);

    public synchronized void on(int status) {
        try {
            while (s != status - 1) {
               wait();
            }
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        s = status;
        notifyAll();
    }
}

class ChildHelper {
    public static void loadSomeClass(boolean useLdc) throws ClassNotFoundException {
        Class<?> clazz;
        if (useLdc) {
            clazz = Helper2.class;
        } else {
            clazz = ChildHelper.class.getClassLoader().loadClass("Helper2");
        }
        System.out.println(clazz + ": " + clazz.getClassLoader());
    }
}

class Helper2 {
    public static void loadSomeClass() {
        System.out.println("Helper2");
    }
}
public class Main {
    static Unsafe unsafe;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            unsafe = (Unsafe)f.get(null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static boolean checkLockIsNotTaken(Object obj) {
        boolean locked = false;
        try {
            locked = unsafe.tryMonitorEnter(obj);
        } finally {
            if (locked) unsafe.monitorExit(obj);
        }
        return !locked;
    }

    public static void main(String[] args) throws Exception {
        boolean useLdc = args.length != 0 && args[0].equals("useLdc");
        TimingHelper timing = new TimingHelper();
        URL[] urls = new URL[]{ Main.class.getProtectionDomain().getCodeSource().getLocation() };
        Object lock = new Object();
        Loader child = new Loader(lock, urls, timing);
        Method childHelpMethod = child.loadClass(ChildHelper.class.getName()).getMethod("loadSomeClass", boolean.class);
        childHelpMethod.setAccessible(true);
        Runnable childHelp = () -> {
            try {
                childHelpMethod.invoke(null, useLdc);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        };

        new Thread(() -> {
            childHelp.run();
        }).start();

        timing.on(2);
        if (checkLockIsNotTaken(child)) System.out.println("lock of loader already token");
        if (checkLockIsNotTaken(lock)) System.out.println("lock of lock object already token");
        timing.on(3);
    }
} 