第五章线程中断与等待唤醒机制

线程中断机制

面试题

void interrupt() 中断此线程
static boolean interrupted() 测试当前线程是否已被中断
boolean isInterrupted() 测试此线程是否已经被中断
  • 三个方法了解过吗?用在哪?
  • 如何停止一个运行中的线程?
  • 如何中断一个运行中的线程??

什么是中断机制?

首先

  • 一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。所以,Thread.stop, Thread.suspend, Thread.resume 都已经被废弃了。
方法名 static 功能说明
stop() 停止线程运行
suspend() 挂起(暂停)线程运行
resume() 恢复线程运行

其次

  • 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。
  • 因此,Java提供了一种用于停止线程的协商机制——中断。
    • 中断只是一种协作协商机制,Java没有给中断增加任何语法,中断的过程完全需要程序员自己实现。

**定义:**中断一个正在运行的线程,(run方法还没有执行结束),普通线程会在run方法执行结束后自动停止,我们的中断其实就是更改线程的状态,想让线程终止,只有run方法执行完毕,就自然终止了

如果我们想中断一个线程,你需要手动的调用该线程的interrupt方法,该方法也仅仅是将线程的中断标识设成true,这里并没有真正的让线程停止运行,接着你需要自己写代码不断地检测当前线程的标识位,如果为true,表示别的线程要求这条线程中断,此时究竟该做什么需要你自己写代码实现。

  • 每个线程对象中都有一个标识,用于表示线程是否被中断;该标识位为true表示中断,为false表示未中断;

  • 通过调用线程对象的interrupt方法将该线程的标识位设为true;可以在别的线程中调用,也可以在自己的线程中调用。

  • eg.顾客在无烟餐厅中吸烟,服务员希望他别吸烟了,不是强行停止他吸烟,而是给他的标志位打为true,具体的停止吸烟还是要顾客自己停止。(体现了协商机制)

实现三种中断方式

  • 通过共享变量进行中断

  • 通过AtomicBoolean(原子布尔型)

  • 使用Thread.interrupt()静态方法进行中断

通过一个volatile变量实现

  • volatile保证了可见性,t2修改了标志位后能马上被t1看到
public class InterruptDemo {
    
    
    static volatile boolean isStop = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(()->{
    
    
            while (true){
    
    
                if (isStop){
    
    
                    System.out.println(Thread.currentThread().getName()+"\t isStop被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
            }
        },"t1").start();
        Thread.sleep(20);
        new Thread(()->{
    
    
            isStop = true;
        },"t2").start();
    }
}
t1 ------hello volatile
t1 ------hello volatile
t1 ------hello volatile
t1 ------hello volatile
t1	 isStop被修改为true,程序终止

通过AtomicBoolean(原子布尔型)

public class InterruptDemo {
    
    
    static volatile boolean isStop = false;
    static AtomicBoolean atomicBoolean = new AtomicBoolean(false);
    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(()->{
    
    
            while (true){
    
    
                if (atomicBoolean.get()){
    
    
                    System.out.println(Thread.currentThread().getName()+"\t atomicBoolean被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
            }
        },"t1").start();
        Thread.sleep(20);
        new Thread(()->{
    
    
           atomicBoolean.set(true);
        },"t2").start();
    }
}
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1 ------hello volatile
//t1   atomicBoolean被修改为true,程序终止

通过Thread类自带的中断api方法实现

public class InterruptDemo {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        Thread t1 = new Thread(() -> {
    
    
            while (true) {
    
    
                if (Thread.currentThread().isInterrupted()) {
    
    
                    System.out.println(Thread.currentThread().getName() + "\t isInterrupted()被修改为true,程序终止");
                    break;
                }
                System.out.println("t1 ------hello volatile");//----------------------如果没停止,那就一直打印
            }
        }, "t1");
        t1.start();
        Thread.sleep(20);
        new Thread(()->{
    
    
            t1.interrupt();
        },"t2").start();
    }
}
  • public void interrupt() 实例方法,实例方法interrupt()仅仅是设置线程的中断状态为true,发起一个协商而不会立刻停止线程
  • public static boolean interrupted() 静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:
    • 1 返回当前线程的中断状态
    • 2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
  • public boolean isInterrupted() 实例方法,判断当前线程是否被中断(通过检查中断标志位)

API源码分析

实例方法interrupt(),没有返回值

//Thread.java
public void interrupt() {
        if (this != Thread.currentThread())
            checkAccess();

        synchronized (blockerLock) {
            Interruptible b = blocker;
            if (b != null) {
                interrupt0();           // Just to set the interrupt flag----调用了interrupt0()方法
                b.interrupt(this);
                return;
            }
        }
        interrupt0();
    }

//Thread.java
    /* Some private helper methods */
    private native void setPriority0(int newPriority);
    private native void stop0(Object o);
    private native void suspend0();
    private native void resume0();
    private native void interrupt0();  //---------------------------调用了c底层
    private native void setNativeName(String name);

实例方法isInterrupted,返回布尔值

//Thread.java
public boolean isInterrupted() {
    return isInterrupted(false);
}
//Thread.java
private native boolean isInterrupted(boolean ClearInterrupted);//也调用了c底层

具体来说,当对一个线程,调用 interrupt() 时:

  • 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。所以, interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行。

  • 当线程调用sleep/wait/join等方法处于阻塞的时候,收到thread.interrupt(),就会抛出一个中断异常,InterruptedException,当抛出这个异常的时候(无论是使用那种判断方式),当前线程的中断状态会被清除(也就是Thread类中那个特别设置的中断属性)

  • (中断不活动的线程不会产生任何影响,看下面案例)

当前线程的中断标识为true,是不是线程就立刻停止?

    • 仅仅设置了一个中断状态,并没有真正的停止线程
  • 看看中断是否会立即停止这个300的线程
    • 否,虽然中断标志位变了。但是i一直在循环
public class InterruptDemo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 300; i++) {
                System.out.println("---------" + i);
            }
            System.out.println("after t1.interrupt()---第2次----"+Thread.currentThread().isInterrupted());
        }, "t1");
        t1.start();
        System.out.println("before t1.interrupt()----"+t1.isInterrupted());
        t1.interrupt();
        Thread.sleep(3);
        System.out.println("after t1.interrupt()---第1次---"+t1.isInterrupted());
        Thread.sleep(3000);
        t1.interrupt();
        System.out.println("after t1.interrupt()---第3次---"+t1.isInterrupted());
    }
}
//before t1.interrupt()----false
//---------0
//---------1
//---------2
//---------3
//....
//---------136
//after t1.interrupt()---第1次---true    ------此处中断标志位设置为了true,但是t1仍然在运行
//---------137
//---------298
//---------299
//after t1.interrupt()---第2次----true
//after t1.interrupt()---第3次---false//中断不活动的线程不会产生任何影响,线程结束后应该是自动变为了false
  • before t1.interrupt()----false
    • 一个线程的默认中断标识位为false
  • after t1.interrupt()—第1次—true
    • 我们线程的中断标识位为true,但是线程并没有结束,还在打印
  • after t1.interrupt()—第2次----true
    • 再次证明,虽然中断标识位变成了true,但是线程并没有结束
  • after t1.interrupt()—第3次—false
    • 这次输出,t1线程已经结束了,虽然我们再次进行调用interrupt(),但是我们的中断标识位还是false
    • 说明中断不活动的线程不会产生任何影响

后手案例-深入

  • 在我们基本中断程序的骨架上 + 一个sleep阻塞
  • 中断异常 且 会导致程序无限循环.
public class InterruptDem02 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
                while (true){
                    if (Thread.currentThread().isInterrupted()){
                        System.out.println(Thread.currentThread().getName()+"\t"+
                                "中断标志位:"+Thread.currentThread().isInterrupted()+"程序终止");
                        break;
                    }
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                         // Thread.currentThread().interrupt();  假如加了这个,程序可以终止,只会爆异常
                        e.printStackTrace();
                    }
                    System.out.println("-----hello InterruptDemo03");
                }
        }, "t1");
        t1.start();
        TimeUnit.MILLISECONDS.sleep(1);
        new Thread(()-> t1.interrupt(),"t2").start();
    }
}
/*java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.lsc.day04.InterruptDem02.lambda$main$0(InterruptDem02.java:23)
	at java.lang.Thread.run(Thread.java:748)
-----hello InterruptDemo03
-----hello InterruptDemo03
-----hello InterruptDemo03
-----hello InterruptDemo03
-----hello InterruptDemo03
-----hello InterruptDemo03
*/
  • 当处于阻塞状态(例如处于sleep, wait, join 等状态)的线程收到中断协商,(在别的线程中调用当前线程对象的interrupt方法),那么线程将立即退出被阻塞状态且会将中断标识位清除,会直接爆出异常(InterruptedException),但是线程并没有结束。
  • 1 中断标志位 默认是false
  • 2 t2 ----->t1发出了中断协商,t2调用t1.interrupt(),中断标志位true
  • 3 中断标志位true,正常情况下,程序停止,-
  • 4 中断标志位true,异常情况下,InterruptedException,将会把中断状态清除,并且将收到InterruptedException。中断标志位false导致无限循环。
  • 5 在catch块中,需要再次给中断标志位设置为true,2次调用停止

静态方法Thread.interrupted(),谈谈你的理解

public static boolean interrupted()
静态方法,Thread.interrupted();判断线程是否被中断,并清除当前中断状态这个方法做了两件事:

  • 1 返回当前线程的中断状态
  • 2 将当前线程的中断状态设为false(这个方法有点不好理解,因为连续调用两次的结果可能不一样。)
public class InterruptDemo3 {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
        System.out.println("-----1");
        Thread.currentThread().interrupt();//中断标志位设置为true
        System.out.println("-----2");
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
        System.out.println(Thread.currentThread().getName()+"\t"+Thread.interrupted());
    }
}
main	false
main	false
-----1
-----2
main	true
main	false

看下源码,interrupted()对比isInterrupted()

  • public static boolean interrupted() {
         return currentThread().isInterrupted(true);
    }
        
    private native boolean isInterrupted(boolean ClearInterrupted);
    
    
  • public boolean isInterrupted() {
          
          
         return isInterrupted(false);
    }
    
    private native boolean isInterrupted(boolean ClearInterrupted);
    
    • 他们在底层都调用了native方法isInterrupted。

    • 只不过传入参数ClearInterrupted一个传参传了true,一个传了false。

  • 静态方法interrupted() 中true表示清空当前中断状态。

  • 实例方法isInterrupted 则不会。

LockSupport是什么

  • 官方解释:用于创建锁和其他同步类的基本线程阻塞原语

核心就是park()和unpark()方法

  • park()方法是阻塞线程

  • unpark()方法是解除阻塞线程

线程等待唤醒机制

在我们多线程开发的时候,有时候会有让一些线程先执行的,这些线程结束后,其他线程再继续执行,列如我们生活中打球,球场的每个人都是一个线程,那一个球员必须先传球给另一个球员,那么另一个球员才能投篮,这就是一个线程的一个动作执行完,另一个线程的动作才能执行

  • 使用Object中的wait()方法让线程等待,使用Object中的notify()方法唤醒线程

  • 使用JUC包中Condition的wait()方法让线程等待,使用signal()方法唤醒线程

  • LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程

Object的wait和notify

Wait方法

public final void wait() throws InterruptedException {
    
    
   wait(0);
}
public final void wait(long timeout, int nanos) throws InterruptedException {
    
    
   if (timeout < 0) {
    
    
      throw new IllegalArgumentException("timeout value is negative");
   }
   if (nanos < 0 || nanos > 999999) {
    
    
      throw new IllegalArgumentException("nanosecond timeout value out of range");
   }
	if (nanos > 0) {
    
    
      timeout++;
    }
	wait(timeout);
}
  • 痴汉方法,死等,线程进入阻塞态(WAITING),直到有其他线程调用notify方法唤醒

  • 等待一段时间,若再该时间内线程被唤醒,则继续执行,若超过了相应时间还没有其他线程唤醒此线程,此线程不再等待,恢复执行等待方法做的事

等待方法做的事

  • 调用wait方法的前提是获得这个对象的锁(synchronized对象锁,如果有多个线程取竞争这个锁,只有一个线程获得锁,其他线程会处于阻塞队列)

  • 使当前执行代码的线程进行等待 . ( 把线程放到等待队列中 )

  • 调用wait方法会释放锁

  • 满足一定条件会重新尝试获得这个锁,被唤醒的之后不是立即恢复执行,而是进入阻塞队列,竞争锁

结束等待的三个方式

  • 其他线程调用该对象的 notify 方法.
  • wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
  • 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常

notify

public final native void notify();
public final native void notifyAll();
  • notify()随机唤醒一个处在等待状态的线程
  • notifyAll()唤醒所有处在等待状态的线程
  • 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
  • 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”) 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
public class ObjectWaitAndNotify {
    
    
    public static void main(String[] args) {
    
    
        Object objectLock = new Object();
        new Thread(()->{
    
    
            synchronized (objectLock){
    
    
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
    
    
                    objectLock.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();
        //暂停几秒钟线程
        try {
    
     TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        new Thread(()->{
    
    
            synchronized (objectLock){
    
    
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
            }
        },"t2").start();
    }
}

异常1—去掉synchronized

  • 说明要使用wait和notify必须加synchronized
public class ObjectWaitAndNotify {
    
    
    public static void main(String[] args) {
    
    
        Object objectLock = new Object();
        new Thread(()->{
    
    
//            synchronized (objectLock){
    
    
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
    
    
                    objectLock.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
//            }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();
        //暂停几秒钟线程
        try {
    
     TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        new Thread(()->{
    
    
//            synchronized (objectLock){
    
    
                objectLock.notify();
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
//            }
        },"t2").start();
    }
}
t1	 ---- come in
Exception in thread "t1" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.lsc.day04.ObjectWaitAndNotify.lambda$main$0(ObjectWaitAndNotify.java:20)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "t2" java.lang.IllegalMonitorStateException
	at java.lang.Object.notify(Native Method)
	at com.lsc.day04.ObjectWaitAndNotify.lambda$main$1(ObjectWaitAndNotify.java:31)
	at java.lang.Thread.run(Thread.java:748)

异常2—把notify和wait的执行顺序对换

  • 说明顺序不能对换
public class ObjectWaitAndNotify{
    
    
    public static void main(String[] args){
    
    
        Object objectLock = new Object();
        new Thread(() -> {
    
    
            try {
    
     TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
            synchronized (objectLock) {
    
    
                System.out.println(Thread.currentThread().getName()+"\t ---- come in");
                try {
    
    
                    objectLock.wait();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"---被唤醒了");
        },"t1").start();
        new Thread(() -> {
    
    
            synchronized (objectLock) {
    
    
                objectLock.notify();//这个先执行了
                System.out.println(Thread.currentThread().getName()+"\t ---发出通知");
            }
        },"t2").start();
    }
}
//一直处于循环中

小总结

  • wait和notify方法必须要在同步块或者方法里面,且成对出现使用
  • 先wait后notify才OK,顺序

背后工作原理解析

img

  • 一定要明确锁的资源是谁,引起的竞争的必须是线程调用的锁的对象一定是要一样的,如果竞争的不是同一个锁,那么就不会进入同一个阻塞队列
  • 其只有唤醒线程执行完毕,阻塞队列线程才会有线程能够得到锁
    • 阻塞队列和等待队列怎么理解,比如当前t1线程获得了锁资源,那么t2,t3如果想竞争这个锁,t2,t3就得处于阻塞队列,当t1线程得到锁并调用了wait方法进入了等待状态进入等待队列,然后释放了锁资源,那么t2和t3就会去竞争锁资源,然后其中获得一个,依次类推,当三个线程都处于等待队列,当调用了notify线程,等待队列其中线程一个进入阻塞队列,但是阻塞队列就算只有一个线程,也不会立即得到锁,因为执行notify线程也会占用锁,必须等notify线程结束,释放锁
    • 如果是调用notifyAll,就会将这三个线程都放入阻塞队列,然后进行竞争锁资源

wait和sleep的区别

  • 其实理论上 wait 和 sleep 完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间, 唯一的相同点就是都可以让线程放弃执行一段时间
  • 如果有共性就先介绍共性,如果没有,分别介绍即可
  • wait方法是Object类提供的方法,需要搭配synchroized锁来使用,调用wait方法会释放锁,等待线程会被其他线程唤醒或者超时自动唤醒,唤醒之后需要再次竞争synchronized锁才能继续执行
  • sleep是Thread类提供的方法(不一定要搭配synchronized使用),调用sleep方法进入TIMED_WAITING状态,如果占用锁也不会不会释放锁,时间到了自动唤醒
  • 因为Java锁的目标是对象,而wait需要释放锁,所以针对的目标都是对象,所以把他定义在Object类中,而Sleep()不需要释放锁,所以他针对的目标是线程,所以定义在Thread类中

为什么线程通信的方法 wait(), notify()和 notifyAll()被定义在 Object 类里

  • Java中锁的目标是对象,Java所有类的都继承了Object,Java想让任何对象都可以作为锁,并且 wait(),notify()等方法用于等待对象的锁或者唤醒线程,在 Java 的线程中并没有可供任何对象使用的锁,所以任意对象调用方法一定定义在Object类中。

为什么 wait(), notify()和 notifyAll()必须在同步方法或者同步块中被调用

  • 当一个线程需要调用对象的 wait()方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态(等待队列)直到其他线程调用这个对象上的 notify()方法。
  • 同样的,当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁(在执行完锁的代码内容),以便其他在等待的线程就可以得到这个对象锁。
  • 由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。

Condition接口中的await和signal

public class ConditionAwaitAndSignal {
    
    
    public static void main(String[] args) {
    
    
        ReentrantLock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->{
    
    
            lock.lock();
            try {
    
    
                System.out.println(Thread.currentThread().getName()+"\t-----come in");
                condition.await();
                System.out.println(Thread.currentThread().getName()+"\t -----被唤醒");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lock.unlock();
            }
        },"t1").start();
        //暂停几秒钟线程
        try {
    
     TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {
    
     e.printStackTrace(); }
        new Thread(()->{
    
    
            lock.lock();
            try {
    
    
                condition.signal();
            }catch (Exception e){
    
    
                e.printStackTrace();
            }
            finally {
    
    
                lock.unlock();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"我要进行唤醒");
        },"t2").start();
    }
}

异常1原理同上

  • 仍然返回IllegalMonitorStateException

异常2 原理同上

  • 仍然在不停的循环

小总结

  • await和notify类似于上面wait和notify

  • Condition中的线程等待和唤醒方法,需要先获取锁

  • 一定要先await后signal,不能反了

Object和Condition使用的限制条件

总结

  • 线程先要获得并持有锁,必须在锁块(synchronized或lock)中

  • 必须要先等待后唤醒,线程才能够被唤醒

LockSupport类中的park等待和unpark唤醒

是什么

  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作

官网解释

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

  • LockSupport类使用了一种名为Permit(许可) 的概念来做到阻塞和唤醒线程的功能, 每个线程都有一个许可(permit),

  • permit(许可)只有两个值1和0,

    • 默认是0。
    • 0 是阻塞
    • 1是唤醒
  • 可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。

阻塞

  • park()/park(Object blocker)

调用LockSupport.park()时,发现它调用了unsafe类,并且默认传了一个0

public static void park() {
    
    
     UNSAFE.park(false, 0L);
}
public native void park(boolean var1, long var2);
  • permit默认是零,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为零并返回。

唤醒

调用LockSupport.unpark();时,也调用了unsafe类

public static void unpark(Thread thread) {
    
    
        if (thread != null)
            UNSAFE.unpark(thread);
}
public native void unpark(Object var1);
  • 调用unpark(thread)方法后,就会将thread线程的许可permit设置成1
    • 注意多次调用unpark方法,不会累加,permit值还是1
  • 会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
public class LockSupportParkAndUnPark {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName()+"\t----------come in");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了");
        }, "t1");
        t1.start();
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName()+"\t----------come in");
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"\t----------我来唤醒了");
        }, "t2").start();
    }
}
t1	----------come in
t2	----------come in
t2	----------我来唤醒了
t1	----------被唤醒了

之前错误的先唤醒后等待,LockSupport照样支持

public class LockSupportParkAndUnPark {
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName()+"\t----------come in");
            try {
    
    
                Thread.sleep(3000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了");
        }, "t1");
        t1.start();
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName()+"\t----------come in");
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"\t----------我来唤醒了");
        }, "t2").start();
    }
}
t1	----------come in
t2	----------come in
t2	----------我来唤醒了
t1	----------被唤醒了

sleep方法3秒后醒来,执行park无效,没有阻塞效果,解释如下。先执行了unpark(t1)导致上面的park方法形同虚设无效,时间是一样的

  • 类似于高速公路的ETC,提前买好了通行证unpark,到闸机处直接抬起栏杆放行了,没有park拦截了。
public class LockSupportDemo
{
    
    
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(()->{
    
    
            try {
    
    TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
    
    e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"\t----------come in"+"\t"+System.currentTimeMillis());
            LockSupport.park();
            LockSupport.park();
            System.out.println(Thread.currentThread().getName()+"\t----------被唤醒了"+"\t"+System.currentTimeMillis());
        },"t1");
        t1.start();

        new Thread(()->{
    
    
            LockSupport.unpark(t1);
            LockSupport.unpark(t1);
            System.out.println(Thread.currentThread().getName()+"\t-----发出通知,去唤醒t1");
        },"t2").start();
    }
}
//t2  -----发出通知,去唤醒t1
//t1  ----------come in  1654750970677--------------------卡在这里了

  • 一个线程的通行证只能有一个

小总结

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

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

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

  • Lock Support和每个使用它的线程都有一个许可(permit) 关联。

  • 每个线程都有一个相关的permit, permit最多只有一个, 重复调用unpark也不会积累凭证。

面试题
为什么可以突破wait/notify的原有调用顺序?

  • 因为unpark获得了一个凭证, 之后再调用park方法, 就可以名正言顺的凭证消费, 故不会阻塞。
  • 先发放了凭证后续可以畅通无阻。

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

  • 因为凭证的数量最多为1, 连续调用两次unpark和调用一次unpark效果一样, 只会增加一个凭证;而调用两次park却需要消费两个凭证, 证不够, 不能放行。

wait和notify正确的使用姿势

问题提出,小南必须要有烟才能干活,其他人干活不需要烟

sleep解决

public class ThreadDemo6 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                System.out.println("有烟吗" + hasCigarette);
                if (!hasCigarette) {
    
    
                    System.out.println("没烟,先歇会!");
                    try {
    
    
                        TimeUnit.SECONDS.sleep(2);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("有烟没?" + hasCigarette);
                if (hasCigarette) {
    
    
                    System.out.println("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (room) {
    
    
                    System.out.println("其他人可以干活了");
                }
            }, "其它人").start();
        }
        TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
    
    
            // 这里能不能加 synchronized (room)?
            hasCigarette = true;
            System.out.println("烟到了");
        }, "送烟的").start();
    }
}
有烟吗false
没烟,先歇会!
烟到了
有烟没?true
可以开始干活了
其他人可以干活了
其他人可以干活了
其他人可以干活了
其他人可以干活了
其他人可以干活了
  • 其它干活的线程,都要一直阻塞,效率太低
  • 小南线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
  • 送烟线程不能加锁,加了 synchronized (room) 后,就好比小南在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的
  • 解决方法,使用 wait - notify 机制

wait/notify解决

  public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                System.out.println("有烟吗" + hasCigarette);
                if (!hasCigarette) {
    
    
                    System.out.println("没烟,先歇会!");
                    try {
    
    
                        room.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("有烟没?" + hasCigarette);
                if (hasCigarette) {
    
    
                    System.out.println("可以开始干活了");
                }
            }
        }, "小南").start();
        for (int i = 0; i < 5; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (room) {
    
    
                    System.out.println("可以开始干活了");
                }
            }, "其它人").start();
        }
       TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                hasCigarette = true;
                System.out.println("烟到了");
                room.notify();
            }
        }, "送烟的").start();
    }
有烟吗false
没烟,先歇会!
可以开始干活了
可以开始干活了
可以开始干活了
可以开始干活了
可以开始干活了
烟到了
有烟没?true
可以开始干活了
  • 解决了其它干活的线程阻塞的问题
    • 因为进入wait队列会释放锁
  • 但如果有其它线程也在等待条件呢?

当不止一个线程在wait队列中

加一个小女,她必须等到外卖到了才能干活

public class ThreadDemo6 {
    
    
    static final Object room = new Object();
    static boolean hasCigarette = false;
    static boolean hasTakeout = false;

    public static void main(String[] args) throws InterruptedException {
    
    
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                System.out.println("小南:有烟吗" + hasCigarette);
                if (!hasCigarette) {
    
    
                    System.out.println("小南:没烟,先歇会!");
                    try {
    
    
                        room.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("小南:有烟没?" + hasCigarette);
                if (hasCigarette) {
    
    
                    System.out.println("小南:可以开始干活了");
                }
            }
        }, "小南").start();
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                Thread thread = Thread.currentThread();
                System.out.println("小女:外卖送到了吗"+hasCigarette);
                if (!hasTakeout) {
    
    
                    System.out.println("小女:没外卖,先歇会!");
                    try {
    
    
                        room.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println("小女:外卖送到了吗"+hasCigarette);
                if (hasTakeout) {
    
    
                    System.out.println("小女:可以开始干活了");
                } else {
    
    
                    System.out.println("小女:没干成活...");
                }
            }
        }, "小女").start();
        for (int i = 0; i < 5; i++) {
    
    
            new Thread(() -> {
    
    
                synchronized (room) {
    
    
                    System.out.println("可以开始干活了");
                }
            }, "其它人").start();
        }
       TimeUnit.SECONDS.sleep(1);
        new Thread(() -> {
    
    
            synchronized (room) {
    
    
                hasTakeout = true;
                System.out.println("外卖到了噢!");
                room.notify();
                //room.notifyAll();
            }
        }, "送外卖的").start();
    }

}
小南:有烟吗false
小南:没烟,先歇会!
小女:外卖送到了吗false
小女:没外卖,先歇会!
可以开始干活了
可以开始干活了
可以开始干活了
可以开始干活了
可以开始干活了
外卖到了噢!
小南:有烟没?false
  • 我们发现其实我们是外卖送到了,但是唤醒的确是等待烟的小南线程,导致程序卡住,小女线程就一直等
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll

优化if

if (!hasCigarette) {
    
    
	log.debug("没烟,先歇会!");
	try {
    
    
		room.wait();
	} catch (InterruptedException e) {
    
    
		e.printStackTrace();
	}
}
  • 虽然notify会将需要的线程唤醒,但是还是存在问题,我们本来不应该将小南唤醒,但是还是给他唤醒了,还导致小南最后没有干成活
while (!hasCigarette) {
    
    
	log.debug("没烟,先歇会!");
	try {
    
    
		room.wait();
	} catch (InterruptedException e) {
    
    
		e.printStackTrace();
	}
}
  • 如果我们把if改成while,就算我们勿把不应该唤醒的线程唤醒了,还是能够让它再次进入等待队列

总结

  • 在使用wait/notify时,应该使用notifyAll,防止没有把该唤醒的线程唤醒
  • 配合wait/notify使用时,应该用while代替if,为了防止当不应该被唤醒时而被唤醒,能够再次进入等待队列

练习

固定运行顺序

比如,必须先 2 后 1 打印

public class Demo1 {
    
    
    //用来同步的对象
    static Object obj = new Object();
    //t2 运行标记 ,代表t2是否执行过
    static boolean t2runed = false;
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            
            synchronized (obj) {
    
    
                //如果t2没有执行过
                while (!t2runed) {
    
    
                    try {
    
    
                        obj.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                //说明t2执行过
                System.out.println(1);
            }

        });
        Thread t2 = new Thread(() -> {
    
    
            System.out.println(2);
            synchronized (obj) {
    
    
                t2runed = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });
        t1.start();
        t2.start();
    }
}
public class Demo1 {
    
    
    //用来同步的对象
    static Object obj = new Object();
    //t2 运行标记 ,代表t2是否执行过
    static boolean t2runed = false;
    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(() -> {
    
    
            try {
    
     Thread.sleep(1000); } catch (InterruptedException e) {
    
     }
            // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
            LockSupport.park();
            System.out.println("1");
        });
        Thread t2 = new Thread(() -> {
    
    
            System.out.println("2");
            // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
            LockSupport.unpark(t1);
        });
        t1.start();
        t2.start();
    }
}

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

wait/notify版

class SyncWaitNotify {
    
    
    private int flag;
    private int loopNum;

    public SyncWaitNotify(int flag, int loopNum) {
    
    
        this.flag = flag;
        this.loopNum = loopNum;
    }
    public void print(int waitFlag, int nextFlag, String str){
    
    
        for (int i = 0; i < loopNum; i++) {
    
    
            synchronized (this){
    
    
                //没有轮到这个线程打印
                while (flag!=waitFlag){
    
    
                    try {
    
    
                        this.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                System.out.println(str);
                flag=nextFlag;
                this.notifyAll();
            }
        }
    }
}
public class Demo2 {
    
    
    public static void main(String[] args) {
    
    
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        new Thread(()->{
    
    
            syncWaitNotify.print(1,2,"a");
        }).start();
        new Thread(()->{
    
    
            syncWaitNotify.print(2,3,"b");
        }).start();
        new Thread(()->{
    
    
            syncWaitNotify.print(3,1,"c");
        }).start();

    }
}
  • 因为wait/notify只有一个等待队列。但是涉及到多个线程,所以使用boolean类型不能满足判断当前唤醒的线程是否应该运行,还是等待,所以我们用一个int类型来判断——int flag
  • 我们当前线程运行完,需要知道轮到哪个线程运行——int nextFlag

Lock 条件变量版

class AwaitSignal extends ReentrantLock {
    
    
    // 循环次数
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
    
    
        this.loopNumber = loopNumber;
    }
    public void start(Condition first) {
    
    
        this.lock();
        try {
    
    
            System.out.println("start");
            first.signal();
        } finally {
    
    
            this.unlock();
        }
    }
    public void print(String str, Condition current, Condition next) {
    
    
        for (int i = 0; i < loopNumber; i++) {
    
    
            this.lock();
            try {
    
    
                current.await();
                System.out.print(str);
                next.signal();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                this.unlock();
            }
        }
    }
}
public class Demo3 {
    
    
    public static void main(String[] args) {
    
    
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
    
    
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        new Thread(() -> {
    
    
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        new Thread(() -> {
    
    
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        as.start(aWaitSet);
    }
}
  • 因为我们的await和signal,可以通过Condition来获得多个等待队列,那么就可以交替唤醒不同等待队列的线程就可以实现

Park Unpark 版

class SyncPark {
    
    
    private int loopNum;
    private Thread[] threads;

    public SyncPark(int loopNum) {
    
    
        this.loopNum = loopNum;
    }

    public void setThreads(Thread... threads) {
    
    
        this.threads = threads;
    }
    public void print(String str){
    
    
        for (int i = 0; i < loopNum; i++) {
    
    
            LockSupport.park();
            System.out.println(str);
            LockSupport.unpark(nextThread());
        }
    }
    
    private Thread nextThread(){
    
    
        Thread current = Thread.currentThread();
        int index = 0;
        for (int i = 0; i < threads.length; i++) {
    
    
            if (current==threads[i]){
    
    
                index=i;
            }
        }
        if(index < threads.length - 1) {
    
    
            return threads[index+1];
        } else {
    
    
            return threads[0];
        }
    }
    public void start(){
    
    
        for (Thread thread : threads) {
    
    
            thread.start();
        }
        LockSupport.unpark(threads[0]);
    }

}
public class Demo4 {
    
    
    public static void main(String[] args) {
    
    
        SyncPark syncPark = new SyncPark(5);
        Thread t1 = new Thread(() -> {
    
    
            syncPark.print("a");
        });
        Thread t2 = new Thread(() -> {
    
    
            syncPark.print("b");
        });
        Thread t3 = new Thread(() -> {
    
    
            syncPark.print("c");
        });
        syncPark.setThreads(t1,t2,t3);
        syncPark.start();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_50985215/article/details/131178873