前段时间在某求职网站上受邀投递了阿里的某个职位,由于本人工作年限有限,项目经验也缺乏亮点,并没有期望简历能够通过筛选。某个下午,突然接到阿里面试官的电话,进行了简单的交流。面试官告知要先进行笔试,笔试题会在晚上某个时间发至本人的邮箱,3道笔试题,要求在90分钟内完成。通话结束后,既兴奋又惊慌,兴奋是简历通过筛选了,惊慌是没有做好足够的准备。
现在我们来聊聊其中一道考察多线程场景的笔试题目。
笔试题目:有3个线程和1个公共的字符数组。线程1的功能就是向数组输出A,线程2的功能就是向数组输出l,线程3的功能就是向数组输出i。要求按顺序向数组赋值AliAliAli,Ali的个数由线程A函数的参数指定。
笔试和考试一样,看到题目应该先理解题目,审清题目要考察的知识点,整理出答题思路,最后才是写出答案。为了便于区分,将线程1、2、3分别命名为线程A、B、C,本人看到题目时对题目的理解如下:
- 看到3个线程和一个公共的字符数组,想到的关键字就是“多线程”和“临界资源”。
- 线程A的功能就是向数组输出A,线程B的功能是向数组输出l,线程C的功能就是向数组输出i。这说明A、B、C三个线程不仅有自己独特的功能,而且每个线程都要对数组进行写操作。
- 要求按顺序向数组赋值AliAliAli,Ali的个数由线程A函数的参数指定。这说明要使A、B、C三个线程有序对数组进行写操作,而且三个线程输出的字符个数是相等的。
那么,这道笔试题要考察的知识点如下:
- 考点1:多线程编程和临界资源。
- 考点2:多线程访问临界资源时,使用锁机制保证临界资源的安全性。
- 考点3:控制线程访问临界资源的顺序。
如果以为这道题目要考察的知识点只有以上三点。那么,我们正在走向“陷阱”的道路上了。当然,这道题目还有隐蔽的“考点”。这个隐蔽的“考点”就是,如果在主线程中打印出数组的内容以验证程序结果的正确性,那么就要保证主线程要等A、B、C三个子线程任务都完成后才能打印数组的内容。否则,子线程的任务未完成而主线程就去打印数组的内容,程序的结果一定不是我们期望的。
所以,这道笔试题的第四个考点如下:
考点4:主线程要等待所有子线程任务完成后才能执行自己的任务。
至此,我们已经整理出要考察的知识点。接下来的任务,就是设计出编程思路。
- A、B、C三个线程可以考虑使用静态内部类来实现,实现Runnable接口,重写run(),在run()中实现各个线程的任务。
- 使用ReentrantLock定义一个锁,A、B、C三个线程对数组进行写操作时必须先获取锁,从而保证数组的安全性。
- 从题目中不难看出,A线程操作数组时的下标为0、3、6、9等,B线程操作数组时的下标为1、4、7、10等,C线程操作数组时的下标为2、5、8、11等。以A线程为例,可以定义一个起始值为0的下标,使用while循环向数组中输出A,每循环一次index就加3,直至index不小于数组容量total。
- 要求主线程等待所有子线程任务完成可以使用多线程同步工具CountDownLatch,CountDownLatch的初始值等于线程的个数。
如果想要了解CountDownLatch类,可以参考本人的另一篇文章,链接如下:https://blog.csdn.net/zhaoheng314/article/details/81738130
设计出编程思路,我们可以对设计进行实现,实现代码如下:
/**
* @author zhaoheng
* @date 2018年8月24日
*/
public class CharArrayTest {
private static final Logger LOG = LoggerFactory.getLogger(CharArrayTest.class);
// 字符数组
private static char [] charArray = {};
// 同步计数器
private static CountDownLatch countDown = new CountDownLatch(3);
// 定义一个锁
private static ReentrantLock lock = new ReentrantLock();
public static class A implements Runnable {
public A (int num) {
charArray = new char [num*3];
}
@Override
public void run() {
try {
lock.lock();
System.out.println("A抢到锁");
int index = 0;
while (index < charArray.length) {
charArray[index] = 'A';
index+=3;
}
countDown.countDown();
} catch (Exception e) {
LOG.error("线程A执行异常:", e);
} finally {
lock.unlock();
System.out.println("A释放锁");
}
}
}
public static class B implements Runnable {
@Override
public void run() {
try {
lock.lock();
System.out.println("B抢到锁");
int index = 1;
while (index < charArray.length) {
charArray[index] = 'l';
index+=3;
}
countDown.countDown();
} catch (Exception e) {
LOG.error("线程B执行异常:", e);
} finally {
lock.unlock();
System.out.println("B释放锁");
}
}
}
public static class C implements Runnable {
@Override
public void run() {
try {
lock.lock();
System.out.println("C抢到锁");
int index = 2;
while (index < charArray.length) {
charArray[index] = 'i';
index+=3;
}
countDown.countDown();
} catch (Exception e) {
LOG.error("线程C执行异常:", e);
} finally {
lock.unlock();
System.out.println("C释放锁");
}
}
}
public static void main(String[] args) {
System.out.print("请输入要打印的Ali的数量:");
Scanner scanner = new Scanner(System.in);
int inputNum = scanner.nextInt();
//创建一个定长的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(new A(inputNum));
executorService.submit(new B());
executorService.submit(new C());
try {
//阻塞主线程直至线程池中的线程任务执行完毕
countDown.await();
} catch (InterruptedException e) {
LOG.error("阻塞出现异常:", e);
}
System.out.println(charArray);
executorService.shutdown();
scanner.close();
}
}
要打印的Ali的数量从控制台输入,便于进行测试。这里采用四组测试数据,即打印的Ali的数量分别设置为10、100、1000。
打印的Ali的数量设置为10时的测试结果:
请输入要打印的Ali的数量:10
A抢到锁
A释放锁
B抢到锁
B释放锁
C抢到锁
C释放锁
AliAliAliAliAliAliAliAliAliAli
打印的Ali的数量设置为100时的测试结果:
请输入要打印的Ali的数量:100
A抢到锁
A释放锁
B抢到锁
B释放锁
C抢到锁
C释放锁
AliAliAli......AliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAli
打印的Ali的数量设置为1000时的测试结果:
请输入要打印的Ali的数量:1000
A抢到锁
A释放锁
B抢到锁
B释放锁
C抢到锁
C释放锁
AliAliAliAliAli......AliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAliAli
观察3组测试结果(省略了部分输出结果)可以看出,A、B、C三个线程是按照顺序获取锁,按顺序对数组进行写操作。这里简单说明下为什么要测试打印1000个Ali,当打印1000个Ali时,char数组的容量就是3000。当执行打印数组语句时,由于eclipse控制台的缓冲区设置的问题,输出的内容会显示为空白,会误导我们以为程序有错误。可以通过工具栏windows->preferences->Run/Debug->Console修改缓冲区设置。
至此,完成了这道笔试题。从代码的角度再来总结下这道题考察的知识点:
- 如何实现多线程
- 多线程场景如何保证临界资源的安全性
- 多线程同步工具类CountDownLatch的使用场景
至此,对这道笔试题总结完毕。
由于笔主水平有限,笔误或者不当之处还请批评指正。