分享一道阿里笔试题

朋友们许久不见,你们还好吗?

这段时间里,我也悄咪咪的去试了试外面的机会,2 年没有参加面试发现各大厂的面试风格已经悄悄的发生了变化。

前俩年都是喜欢上来一套 JUC 三连炮问到你懵圈为止,要不就是一套 Mysql 事务三连炮问到你瑟瑟发抖。而现在呢,面试官们喜欢揪着你的项目刨根问底。你可能会说,唉我的项目就是一堆 CRUD,没啥可说的。

不用担心,面试官会就你的业务场景,改造一下,现场创造一些难点来让你提出解决方案。你以为想到解决方案就完了吗?too naive!这时候你才刚刚走进面试官的陷阱,接着面试官会开始对你进行惨无人道(都是搬砖的,相煎何太急)的狂轰乱炸。

此时没有准备充分的你,就像一只站在大灰狼面前的小肥羊,孤独无助,任由大灰狼的皮鞭肆虐。

所以朋友们面试之前一定要多多准备,找几家不想去的公司试试水,同时也要多刷刷数据结构,线程方面的算法题。

这不,我在阿里第一轮笔试的结束的时候就碰到了这么一道题:

使用 3 个线程 A,B,C 来按顺序依次打印 1 – 100 的数字。

效果为:A => 1,B => 2,C => 3,A => 4,B => 5,C => 6,A => 7,…..

相信 synchronized 使用熟练的同学可以很快的输出如下一个解决方案:

public class   ThreadPrint   {

private static int number =  1 ;

private static final Object LOCK =  new Object();

public static void main (String[] args) {

Thread a =  new Thread( new Runnable(){

@Override

public void run () {

while ( true ) {

synchronized (LOCK) {

if (number >  100 ) {

System.out.println( “A over” );

LOCK.notifyAll();

break ;

}

if (number %  3 ==  1 ) {

System.out.println(Thread.currentThread().getName() +  ” => “ + number);

number++;

else {

try {

LOCK.notifyAll();

LOCK.wait();

catch (Exception ex) {

System.out.println(ex);

}

}

}

}

}

},  “Thread-A” );

a.start();

Thread b =  new Thread( new Runnable(){

@Override

public void run () {

while ( true ) {

synchronized (LOCK) {

if (number >  100 ) {

System.out.println( “B over” );

LOCK.notifyAll();

break ;

}

if (number %  3 ==  2 ) {

System.out.println(Thread.currentThread().getName() +  ” => “ + number);

number++;

else {

try {

LOCK.notifyAll();

LOCK.wait();

catch (Exception ex) {

System.out.println(ex);

}

}

}

}

}

},  “Thread-B” );

b.start();

Thread c =  new Thread( new Runnable(){

@Override

public void run () {

while ( true ) {

synchronized (LOCK) {

if (number >  100 ) {

System.out.println( “C over” );

LOCK.notifyAll();

break ;

}

if (number %  3 ==  0 ) {

System.out.println(Thread.currentThread().getName() +  ” => “ + number);

number++;

else {

try {

LOCK.notifyAll();

LOCK.wait();

catch (Exception ex) {

System.out.println(ex);

}

}

}

}

}

},  “Thread-C” );

c.start();

}

}

当你乐呵呵的告诉面试官写完了的时候,他会微微点头告诉你,嗯,首先这是一个可以 work 的方案,但是呢代码稍微有些冗余,可以优化一下吗。

这三个线程的内部运转逻辑基本一致,唯一区别就是取模运算时候的值不同,因此我们可以将代码改进一下:

public class   ThreadPrint   {

private static final Object LOCK =  new Object();

public static void main (String[] args) {

new PrintThread( 1“Thread-A” , LOCK).start();

new PrintThread( 2“Thread-B” , LOCK).start();

new PrintThread( 0“Thread-C” , LOCK).start();

}

}

class PrintThread extends Thread {

private int mod;

private String name;

private static int number =  1 ;

private Object LOCK;

public PrintThread ( int  mod, String name, Object lock) {

this .mod = mod;

this .name = name;

this .LOCK = lock;

}

@Override

public void run () {

while ( true ) {

synchronized (LOCK) {

if (number >  100 ) {

System.out.println(name +  “over” );

LOCK.notifyAll();

break ;

}

if (number %  3 == mod) {

System.out.println(name +  ” => “ + number);

number++;

else {

try {

LOCK.notifyAll();

LOCK.wait();

catch (Exception ex) {

System.out.println(ex);

}

}

}

}

}

}

当你坑此坑次的精简了代码后,面试官笑嘻嘻的说,嗯,现在精简了不少。突然他脸上漏出了狡黠的笑容,还有其他解法吗。

嗯,我想想,你陷入了沉思。突然灵光一闪,我可以用信号量来替换管程,接着你写出了如下的代码:

import java.util.concurrent.Semaphore;

public class   ThreadPrint   {

private static final Object LOCK =  new Object();

private static final Semaphore semaphore =  new Semaphore( 1false );

public static void main (String[] args) {

new PrintThread( 1“Thread-A” , semaphore).start();

new PrintThread( 2“Thread-B” , semaphore).start();

new PrintThread( 0“Thread-C” , semaphore).start();

}

}

class PrintThread extends Thread {

private int mod;

private String name;

private static int number =  1 ;

private Semaphore semaphore;

public PrintThread ( int  mod, String name, Semaphore semaphore) {

this .mod = mod;

this .name = name;

this .semaphore = semaphore;

}

@Override

public void run () {

while ( true ) {

try {

semaphore.acquire();

if (number >  100 ) {

System.out.println(name +  “over” );

semaphore.release();

break ;

}

if (number %  3 == mod) {

System.out.println(name +  ” => “ + number);

number++;

}

semaphore.release();

catch (Exception e) {

e.printStackTrace();

}

}

}

}

没错,使用管程可以解决的线程间互斥问题,采用信号量一样可以解决。当你认为终于可以告一段落的时候,面试官仍然不会放过你,还有其他方法吗?

你假装思索片刻,奥奥,还有一种办法可以使用 ReentrantLock 来解决:

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class   ThreadPrint   {

private static final Lock lock =  new ReentrantLock( false );

public static void main (String[] args) {

new PrintThread( 1“Thread-A” , lock).start();

new PrintThread( 2“Thread-B” , lock).start();

new PrintThread( 0“Thread-C” , lock).start();

}

}

class PrintThread extends Thread {

private int mod;

private String name;

private static int number =  1 ;

private Lock lock;

public PrintThread ( int  mod, String name, Lock lock) {

this .mod = mod;

this .name = name;

this .lock = lock;

}

@Override

public void run () {

while ( true ) {

try {

lock.lock();

if (number >  100 ) {

System.out.println(name +  “over” );

break ;

}

if (number %  3 == mod) {

System.out.println(name +  ” => “ + number);

number++;

}

catch (Exception e) {

e.printStackTrace();

finally {

lock.unlock();

}

}

}

}

你飞速的敲击键盘,将原本采用 Semaphore 的方案替换为了 ReentrantLock,心里想,这下总该满意了吧。

然而并不像你想象的那么简单,面试官再次漏出狡黠的笑容,你现在的方案呢存在一个问题,就是三个线程会同时争抢资源,这样会导致某个线程抢占到了锁资源,但是又没到它打印的数字,有没有办法优化一下这个问题呢?

你陷入了沉思,不同时竞争锁,打印又是有序的,那就由 A 线程打印完数字后,A 去唤醒 B,然后 A进入睡眠;接着 B 打印完数字唤醒 C,然后 B 进入睡眠;最后 C 打印数字,然后唤醒 A,接着 C 进入睡眠,以此类推即可完成循环打印,并且同时只有一个线程在运行。

那使用什么方式来实现呢,聪明的你一定想到了 Condition:

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

import java.util.concurrent.locks.ReentrantLock;

public class   ThreadPrint   {

private static final Lock lock =  new ReentrantLock( false );

private static Condition cOver = lock.newCondition();

private static Condition aOver = lock.newCondition();

private static Condition bOver = lock.newCondition();

public static void main (String[] args) {

new PrintThread( 1“Thread-A” , lock, cOver, bOver).start();

new PrintThread( 2“Thread-B” , lock, aOver, cOver).start();

new PrintThread( 0“Thread-C” , lock, bOver, aOver).start();

}

}

class PrintThread extends Thread {

private int mod;

private String name;

private static int number =  1 ;

private Lock lock;

private Condition wait;

private Condition notify;

public PrintThread ( int  mod, String name, Lock lock, Condition wait, Condition notify) {

this .mod = mod;

this .name = name;

this .lock = lock;

this .notify = notify;

this .wait = wait;

}

@Override

public void run () {

while ( true ) {

try {

lock.lock();

if (number >  100 ) {

System.out.println(name +  “over” );

notify.signal();;

break ;

}

if (number %  3 == mod) {

System.out.println(name +  ” => “ + number);

number++;  

}

notify.signal();

wait.await();

catch (Exception e) {

e.printStackTrace();

finally {

lock.unlock();

}

}

}

}

相信当你作出如上的解决方案后,面试官应该会漏出慈母般的微笑,并且伴随着频频点头,心里已经对你扎实的基本功竖起来大拇指。

如上即为我们今天要介绍的这道并发编程面试题,里面总共采用了四种方法去解决这个问题,层层递进不断优化。

如果你对上面的几种解法还不能烂熟于心,那你要抓紧巩固哦,不然容易给面试官留下一个理论王者的面评就不好啦。