天天看点

Java多线程篇--线程的等待通知

在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java多线程中面试的可能会出现的内容-等待通知。该部分内容是多线程的核心内容,当然也是阻塞队列实现的底层原理。在面试中碰到的概率不大,实际开发中使用的不多(更多的直接使用阻塞队列),同时本文还会描述几个线程相关的内容,这些内容在面试中可能会出现。如果对java多线程感兴趣的同学可以看下公众号里多线程系列的文章,也许会对你有些帮助。

Java如何创建多线程?Thread的run方法和start方法的区别是什么?

在java语言中,多线程的创建方式有以下几种:

继承thread类:继承thread并且重写run方法,这种方法基本很少被使用。

public class CreateThread extends Thread{

@Override

public void run(){

//do something

}

}
           

实现runnable接口:因为thread类中提供了用runnable创建的构造函数,并且thread本身也是实现runnable接口,所以使用runnable创建相对比继承thread会来的方便一点。当然核心依然是这个run函数的实现。

public class CreateThread implements Runnable{

@Override

public void run() {

// do something

}

}
           

实现callable接口+FutureTask:这种方式能够实现的关键点在于FutureTask实现了RunnaleFuture接口,而RunnaleFuture实现了Runnale接口。

public class CreateThread implements Callable<Void>{

@Override

public Void call() throws Exception {

// do something

return null;

}

}

public static void main(String[] args) {

CreateThread createThread = new CreateThread();

FutureTask<Void> futureTask = new FutureTask<>(createThread);

Thread thread = new Thread(futureTask);

thread.start();

return ;

}
           

从上面三种实现手段来看,继承和实现都可以创建新的线程,而runnable和callable都是实现run方法,区别在于callable可以带返回值。上面三个方法都是对run方法的实现,所以run函数才是重点要做的内容,那是不是可以直接调用run函数。Run函数和start函数的区别是啥呢?

我们先看下start方法哈:

Java多线程篇--线程的等待通知

Start方法核心方法是start0。Start0是一个native方法,native关键字是一个本地方法的意思,换句话说是用其他语言实现的,在thread类中有很多native方法,它们都是基于c++实现的,而start0对应c++函数是JVM_StratThread,该函数对应的启动方法是new JavaThread(&thread_entry, sz),对于thread_entry方法的实现中需要调用java中定义的run方法,其对应的函数如下:

Java多线程篇--线程的等待通知

上面说了一堆c++的逻辑哈,主要的核心点是start方法会在线程创建后调用run方法。这就是问题的核心,如果我们只是调用run方法的话,那就不会新创建线程去执行,直接就是调用方执行该方法。而如果是start方法的话,会创建一个新的线程,然后去执行run函数。

Sleep函数和wait函数的区别是什么?Notify和notifyall的区别是什么?

sleep和wait在一定的场景下都可以让线程等待。其区别在于以下几点:

  1. 使用场景上:sleep可以在任何场景下使用,wait需要在获取锁的情况下使用。
  2. 在锁的程度上:sleep不会释放锁,而wait会释放锁,一般这个问题的核心在于这里。
  3. 出处:sleep来自于thread类,wait是object类的函数。

Notify和notifyall的区别在于:当wait函数调用的时候,线程释放锁,并且加入等待队列中,notify和notifyall都可以唤醒等待队列中的线程,notifyall是唤醒所有线程,将它们添加到同步队列,notify是唤醒其中的一个,将其添加到同步队列。

condition中线程是如何被唤醒的?什么是等待队列?

Condition的内部有个ConditionObject,ConditionObject是一个条件变量,主要是结合signal和await进行事件的等待通知功能,和Object的wait、notify一样,但是使用上比wait、notify更灵活。内部的实现有个队列维持一个等待的状态(称为等待队列),当获取到ConditionObject的时候,进入到同步队列。同步队列就是AQS中的锁的关键,获取到锁的开始执行操作。当线程调用await方法的时候,进入到等待队列中。下面我们看下等待队列和同步队列的关系。

Java多线程篇--线程的等待通知

从上图看出,一个lock可以对应多个condition,每个condition都有一个等对列队与之对应。当队列signal或者signalall的时候会进入同步队列。当线程await的时候,进入等待队列。下面从源码看下await和signal的实现。

await函数:

public final void await() throws InterruptedException {  
            if (Thread.interrupted())  
                throw new InterruptedException();  
            Node node = addConditionWaiter();//添加到等待队列中  
            int savedState = fullyRelease(node);//释放锁  
            int interruptMode = 0;  
            while (!isOnSyncQueue(node)) {//如果不是在同步队列  
                LockSupport.park(this);  
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)//是否是中断  
                    break;  
            }  
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  
                interruptMode = REINTERRUPT;  
            if (node.nextWaiter != null) // 如果取消清除等待者  
                unlinkCancelledWaiters();  
            if (interruptMode != 0)  
                reportInterruptAfterWait(interruptMode);  
        }  
           

过程主要有以下几个流程:

  1. 添加到等待队列,这个和之前的添加到同步队列中类似的操作,往尾节点CAS添加
  2. 释放锁
  3. 不断循环判断是否不在同步队列中,循环过程判断是否取消等操作

Signal函数:

public final void signal() {  
            if (!isHeldExclusively())//如果是独占的  
                throw new IllegalMonitorStateException();//抛出监控异常  
            Node first = firstWaiter;//获取第一个等待者  
            if (first != null)  
                doSignal(first);//唤醒第一个等待者  
        }  
  
private void doSignal(Node first) {  
            do {  
                if ( (firstWaiter = first.nextWaiter) == null)//移动节点的指针关系  
                    lastWaiter = null;  
                first.nextWaiter = null;  
            } while (!transferForSignal(first) &&//如果转换成signal状态不成功就继续下一个节点  
                     (first = firstWaiter) != null);  
        }  
final boolean transferForSignal(Node node) {  
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))//设置节点的等待状态  
            return false;  
        Node p = enq(node);//将节点添加到同步队列  
        int ws = p.waitStatus;  
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))//设置signal的状态  
            LockSupport.unpark(node.thread);  
        return true;  
    }  
           

SignalAll函数:

public final void signalAll() {  
            if (!isHeldExclusively())  
                throw new IllegalMonitorStateException();  
            Node first = firstWaiter;  
            if (first != null)  
                doSignalAll(first);//从第一个开始唤醒  
        }  
 private void doSignalAll(Node first) {  
            lastWaiter = firstWaiter = null;  
            do {  
                Node next = first.nextWaiter;  
                first.nextWaiter = null;  
                transferForSignal(first);//唤醒第一个节点  
                first = next;  
            } while (first != null);//依次唤醒  
        }  
           

Signal函数的过程:取出第一个等待者,将其添加到同步队列。

SignalAll 函数:依次取出所有的节点,并添加到同步队列中。

其实在大部分时候,我们一般用signalAll函数。该函数可以防止signal函数唤醒的线程不是我们想要的线程。

等待通知的经典面试题:使用三个线程1,2,3轮流打印10个数,线程1打印1,线程2打印2,线程3打印3,线程1打印4……

这类题型的实现主要是:

  1. 创建一个锁和多个condition,依次轮流唤醒,最后一个唤醒第一个,构成一个环。根据题目要求看每个线程依次做什么事情,
  2. 判断结束条件(比如我的实现是基于deque中是否有值进行)结束条件也很重要,如果还有线程没被唤醒,那就会导致一直结束不了。
  3. 在主函数中唤醒第一个线程开始操作。唤醒第一个的时候,需要等待其他线程均已经进入了等待队列。
import java.util.ArrayDeque;  
import java.util.Deque;  
import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.ReentrantLock;  
import java.util.stream.IntStream;  
  
public class ConditionTest {  
    public static void main(String[] args) {  
        Deque<Integer> deque = new ArrayDeque<>();  
        IntStream.range(1,11).forEach(deque::add);  
        ReentrantLock lock = new ReentrantLock();//创建锁  
        Condition c1 = lock.newCondition();//创建三个等待变量  
        Condition c2 = lock.newCondition();  
        Condition c3 = lock.newCondition();  
        Thread thread1 = new Thread(new PrintInfo(lock,c1,c2,deque));//创建三个线程  
        thread1.setName("thread_1");  
        thread1.start();  
        Thread thread2 = new Thread(new PrintInfo(lock,c2,c3,deque));  
        thread2.setName("thread_2");  
        thread2.start();  
        Thread thread3 = new Thread(new PrintInfo(lock,c3,c1,deque));  
        thread3.setName("thread_3");  
        thread3.start();  
        wakeUpC1(lock,c1);//唤醒第一个线程  
    }  
  
    private static void wakeUpC1(ReentrantLock lock,Condition condition){  
        try {  
            Thread.sleep(100L);//主线程唤醒要等待3个线程都已经进入await状态  
        } catch (InterruptedException e) {    
        }  
        lock.lock();  
        try{  
            condition.signal();//唤醒第一个打印的线程  
        }catch (Exception ex){ }  
        finally {  
            lock.unlock();  
        }  
    }  
  
    private static class PrintInfo implements Runnable{  
        private ReentrantLock lock ;//锁  
        private Condition myCondition;//阻塞本线程的条件变量  
        private Condition signalCondition;//唤醒下一个执行线程的条件变量  
        private Deque data;  
        public PrintInfo(ReentrantLock l,Condition c,Condition s,Deque i){  
            this.myCondition = c;  
            this.data = i;  
            this.lock = l;  
            this.signalCondition = s;  
        }  
        @Override  
        public void run() {  
            lock.lock();//加锁  
            try {  
                while (true) {//死循环  
                    myCondition.await();//先阻塞,等待别人唤醒  
                    if (data.size()>0){//如果还有数据需要打印  
                        System.out.println(Thread.currentThread().getName()  
                                + " print " + data.pollFirst());  
                        signalCondition.signal();//唤醒下一个打印线程  
                    }else {  
                        signalCondition.signal();//没有数据了,唤醒下一个线程  
                        break;//退出循环  
                    }  
                }  
            } catch (Exception ex) { }  
            finally {  
                lock.unlock();//释放锁  
            }  
        }  
    }  
}  
           

Java多线程的事件等待通知在高级面试中可能会出现,不过在笔试的时候,如果要考察多线程的话,这个一般是个重点内容,难度不高,但是实现的时候,容易出错。在实际开发中,基本比较少用到这块,常用的更多是阻塞队列。本文除了线程的等待通知之外,还对线程的基本创建方式进行了分析,这个在面试中不常问,不过,了解一下可能会对你有一定的帮助,毕竟源码中start和run并没有关系,我们通过看cpp的代码发现其中的调用过程。

如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

Java多线程篇--线程的等待通知