【面试】Java线程阻塞和唤醒的几种方式?

前言

三种让线程等待唤醒的方法如下:

  • 方式一:使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程
  • 方式二:使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程
  • 方式三:LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。

下面分别来介绍一下,希望对大家看完有帮助!

一、Object类自带的方法

使用wait()方法来阻塞线程,使用notify()和notifyAll()方法来唤醒线程。

  • 调用wait()方法后,线程将被阻塞,wait()方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll()方法后方能继续执行。

  • notify/notifyAll()方法只是解除了等待线程的阻塞,并不会马上释放监视器锁,而是在相应的被synchronized关键字修饰的同步方法或同步代码块执行结束后才自动释放锁。

默认使用非公平锁,无法修改。

缺点:

  • 使用几个方法时,必须处于被synchronized关键字修饰的同步方法或同步代码块中,否则程序运行时,会抛出IllegalMonitorStateException异常。
  • 线程的唤醒必须在线程阻塞之后,否则,当前线程被阻塞之后,一直没有唤醒,线程将会一直等待下去(对比LockSupport)
public class SynchronizedDemo {
    
    


// 三个线程交替打印ABC
    public static void main(String[] args) {
    
    

        Print print = new Print();

        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printA();
            }
        }, "A").start();
        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printB();
            }
        }, "B").start();
        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printC();
            }
        }, "C").start();
    }
}

class Print {
    
    

    Object object = new Object();
    int num = 1;

    public void printA() {
    
    
        synchronized (object) {
    
    
            try {
    
    
                while (num != 1) {
    
    
                    object.wait();
                }
                for (int i = 0; i < 5; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + "==>A");
                }
                num = 2;
                object.notifyAll();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public void printB() {
    
    
        synchronized (object) {
    
    
            try {
    
    
                while (num != 2) {
    
    
                    object.wait();
                }
                for (int i = 0; i < 10; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + "==>B");
                }
                num = 3;
                object.notifyAll();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    public void printC() {
    
    
        synchronized (object) {
    
    
            try {
    
    
                while (num != 3) {
    
    
                    object.wait();
                }
                for (int i = 0; i < 15; i++) {
    
    
                    System.out.println(Thread.currentThread().getName() + "==>C");
                }
                num = 1;
                object.notifyAll();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

二、Condition接口

使用 JUC 包中 Condition 的await()方法来阻塞线程,signal()/singnalAll()方法来唤醒线程。
需要使用lock对象的newCondition()方法获得Condition条件对象(可有多个)。
可实现公平锁,默认是非公平锁
缺点:

  • 必须被Lock包裹,否则会在运行时抛出IllegalMonitorStateException异常。
  • 线程的唤醒必须在线程阻塞之后
  • Lock的实现是基于AQS,效率稍高于synchronized
public class ConditionDemo {
    
    

// 三个线程交替打印ABC
    public static void main(String[] args) {
    
    
        Print print = new Print();
        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printA();
            }
        }, "A").start();

        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printB();
            }
        }, "B").start();

        new Thread(() -> {
    
    
            while (true) {
    
    
                print.printC();
            }
        }, "C").start();
    }
}

class Print {
    
    

    private Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private int num = 1;

    public void printA() {
    
    

        lock.lock();
        try {
    
    
            while (num != 1) {
    
    
                condition1.await();
            }
            for (int i = 0; i < 5; ++i) {
    
    
                System.out.println(Thread.currentThread().getName() + "==>A");
            }
            num = 2;
            condition2.signal();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void printB() {
    
    

        lock.lock();
        try {
    
    
            while (num != 2) {
    
    
                condition2.await();
            }
            for (int i = 0; i < 10; ++i) {
    
    
                System.out.println(Thread.currentThread().getName() + "==>B");
            }
            num = 3;
            condition3.signal();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }

    public void printC() {
    
    

        lock.lock();
        try {
    
    
            while (num != 3) {
    
    
                condition3.await();
            }
            for (int i = 0; i < 15; ++i) {
    
    
                System.out.println(Thread.currentThread().getName() + "==>C");
            }
            num = 1;
            condition1.signal();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            lock.unlock();
        }
    }
}

三、LockSupport

使用park()来阻塞线程,用unpark()方法来唤醒线程。
这里有一个许可证的概念,许可不能累积,并且最多只能有一个许可,只有1和0的区别。
特点:

  • 使用灵活,可以直接使用
  • 线程唤醒可在线程阻塞之前,因为调用unpark()方法后,线程已经获得了一个许可证(但也只能有一个许可证),之后阻塞时,可以直接使用这个许可证来通行。
  • 效率高
public class LockSupportDemo {
    
    

// 三个线程交替打印ABC
    public static void main(String[] args) throws Exception {
    
    

        Print print = new Print();

        Thread threadA = new Thread(() -> {
    
    
            while (true) {
    
    
                print.printA();
            }
        }, "A");

        Thread threadB = new Thread(() -> {
    
    
            while (true) {
    
    
                print.printB();
            }
        }, "B");

        Thread threadC = new Thread(() -> {
    
    
            while (true) {
    
    
                print.printC();
            }
        }, "C");

        threadA.start();
        threadB.start();
        threadC.start();

        while (true) {
    
    
            LockSupport.unpark(threadA);
            LockSupport.unpark(threadB);
            LockSupport.unpark(threadC);
        }

    }
}

class Print {
    
    

    private int num = 1;

    public void printA() {
    
    
        while (num != 1) {
    
    
            LockSupport.park();
        }
        for (int i = 0; i < 5; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "==>A");
        }
        num = 2;
    }

    public void printB() {
    
    
        while (num != 2) {
    
    
            LockSupport.park();
        }
        for (int i = 0; i < 10; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "==>B");
        }
        num = 3;
    }

    public void printC() {
    
    
        while (num != 3) {
    
    
            LockSupport.park();
        }
        for (int i = 0; i < 15; i++) {
    
    
            System.out.println(Thread.currentThread().getName() + "==>C");
        }
        num = 1;
    }
}

四、相关面试题

  • 为什么可以先唤醒线程后阻塞线程?

    答:先唤醒线程意味着你调用了 unpark() 方法,那么凭证加1,再去阻塞线程,即调用 park() 方法,这个时候有凭证,所以直接消耗掉凭证然后正常退出

  • 为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

    答:唤醒两次意味着调用了两次 unpark() 方法,但是凭证无法累加最多只有 1,然后阻塞两次,即调用两次 park() 方法,需要消费 2 张凭证才能正常退出,但是只有 1 张凭证,所以凭证不够,阻塞。

总结:

LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。

LockSupport 是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根结底,LockSupport 调用的是 Unsafe 类中的 native 方法。

LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞的过程

LockSupport 和每个使用它的线程都有一个许可(permit)关联。 permit 默认是 0。

  • 调用一次 unpark 就加 1 变成 1
  • 调用一次 park 会消费 permit ,也就是将 1 变成 0,同时 park 立即返回。
  • 如果再次调用 park 会变成阻塞(因为 permit 为 0 会阻塞在这里,一直到 permit 为 1),这时候调用 unpark 会把 permit 置为 1
  • 每个线程都有一个相关的 permit,permit 最多只有一个,重复调用 unpark 不会积累凭证。

简单来说:

线程阻塞需要消耗凭证(permit),这个凭证最多只有一个。

  • 当调用 park 方法时
    • 如果有凭证,则会直接消耗掉这个凭证然后正常退出
    • 如果没有凭证,就必须阻塞等待凭证可用
  • 当调用 unpark 方法时
    • 它会增加一个凭证,但凭证最多只能有一个,无法累加。

Guess you like

Origin blog.csdn.net/u011397981/article/details/130167712