有这样一个面试题:多线程打印AB字符

这玩意但凡有点Java基础的人,都会想到这是考察线程waitnotify,那么具体怎么做呢?如果长时间不写多线程程序,冷不丁的来一下,还真写不出。

先来复习下waitnotify的概念:

wait: Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
The current thread must own this object’s monitor.

notify: Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object’s monitor by calling one of the wait methods.This method should only be called by a thread that is the owner of this object’s monitor.

notifyAll: Wakes up all threads that are waiting on this object’s monitor.

根据Javadoc的注释,可以看出wait将会让出锁,进入WAITING状态,直到其他线程调用notify(All),进入AWAKENED状态,在wait最终返回之前,需要获取锁。这意味着,AWAKENED的线程将和BLOCKING状态的线程一起竞争锁,如果竞争不过,继续待在WAITING状态。有几点需要注意:

  • 无论是wait,还是notify(All),都必须在持有锁的状态下调用
  • notify(All)调用后不会释放锁,而是在离开syntronized区域后
  • AWAKENED线程在竞争锁时没有任何优势,和BLOCKING线程优先级一样

wait的使用范式如下:

synchronized (obj) {
   while (<condition does not hold>)
       obj.wait();
   ... // Perform action appropriate to condition
}

之所以使用循环条件判断,是为了防止线程过早唤醒,也就是发出notify(All)时条件谓词为真,到wait返回时,谓词不为真了。另外Javadoc指出,WAITING线程会有一定的几率自己醒来,而不是收到notify(All)的通知,虽然这极少发生。

回到最初的问题,可以启动两个线程,为他们分配一个名字name,分别为A和B,设置一个变量ticket保存着下一个可运行的线程名,只有name == ticket的线程才有权运行,这样只要改变ticket的值就可以控制线程的运行了,具体代码如下:

public class Main {
    public static void main(String[] args) {
        new PrintChar('A').start();
        new PrintChar('B').start();
    }

    private static class PrintChar extends Thread {
        private static final Object lock = new Object();
        private static char running = 'A';
        private char name;

        public PrintChar(char name) {
            this.name = name;
        }

        @Override
        public void run() {
            for (int i = 0; i<10;i++) {
                synchronized (lock) {
                    while (name != running) {
                        try {
                            // System.out.print(" <" + name + " waiting> ");
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print(name);
                    loop();
                    lock.notify();
                }
            }
        }

        private void loop() {
            if (running == 'B') {
                running = 'A';
            } else {
                running += 1;
            }
        }
    }
}

为了观察线程的运行等待状态,我们将注释放开,得到下面的结果,

A <A waiting> B <B waiting> A <A waiting> B <B waiting> A <A waiting> B ...

作为一个问题的延伸,考虑下面的问题:

多线程打印ABCDE

小伙伴们,你想到了吗?