一道笔试题看出你是否掌握多线程

前段时间在某求职网站上受邀投递了阿里的某个职位,由于本人工作年限有限,项目经验也缺乏亮点,并没有期望简历能够通过筛选。某个下午,突然接到阿里面试官的电话,进行了简单的交流。面试官告知要先进行笔试,笔试题会在晚上某个时间发至本人的邮箱,3道笔试题,要求在90分钟内完成。通话结束后,既兴奋又惊慌,兴奋是简历通过筛选了,惊慌是没有做好足够的准备。
现在我们来聊聊其中一道考察多线程场景的笔试题目。

笔试题目:有3个线程和1个公共的字符数组。线程1的功能就是向数组输出A,线程2的功能就是向数组输出l,线程3的功能就是向数组输出i。要求按顺序向数组赋值AliAliAli,Ali的个数由线程A函数的参数指定。

笔试和考试一样,看到题目应该先理解题目,审清题目要考察的知识点,整理出答题思路,最后才是写出答案。为了便于区分,将线程1、2、3分别命名为线程A、B、C,本人看到题目时对题目的理解如下:

  1. 看到3个线程和一个公共的字符数组,想到的关键字就是“多线程”和“临界资源”。
  2. 线程A的功能就是向数组输出A,线程B的功能是向数组输出l,线程C的功能就是向数组输出i。这说明A、B、C三个线程不仅有自己独特的功能,而且每个线程都要对数组进行写操作。
  3. 要求按顺序向数组赋值AliAliAli,Ali的个数由线程A函数的参数指定。这说明要使A、B、C三个线程有序对数组进行写操作,而且三个线程输出的字符个数是相等的。

那么,这道笔试题要考察的知识点如下:

  • 考点1:多线程编程和临界资源。
  • 考点2:多线程访问临界资源时,使用锁机制保证临界资源的安全性。
  • 考点3:控制线程访问临界资源的顺序。

如果以为这道题目要考察的知识点只有以上三点。那么,我们正在走向“陷阱”的道路上了。当然,这道题目还有隐蔽的“考点”。这个隐蔽的“考点”就是,如果在主线程中打印出数组的内容以验证程序结果的正确性,那么就要保证主线程要等A、B、C三个子线程任务都完成后才能打印数组的内容。否则,子线程的任务未完成而主线程就去打印数组的内容,程序的结果一定不是我们期望的。
所以,这道笔试题的第四个考点如下:

考点4:主线程要等待所有子线程任务完成后才能执行自己的任务。

至此,我们已经整理出要考察的知识点。接下来的任务,就是设计出编程思路。

  1. A、B、C三个线程可以考虑使用静态内部类来实现,实现Runnable接口,重写run(),在run()中实现各个线程的任务。
  2. 使用ReentrantLock定义一个锁,A、B、C三个线程对数组进行写操作时必须先获取锁,从而保证数组的安全性。
  3. 从题目中不难看出,A线程操作数组时的下标为0、3、6、9等,B线程操作数组时的下标为1、4、7、10等,C线程操作数组时的下标为2、5、8、11等。以A线程为例,可以定义一个起始值为0的下标,使用while循环向数组中输出A,每循环一次index就加3,直至index不小于数组容量total。
  4. 要求主线程等待所有子线程任务完成可以使用多线程同步工具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修改缓冲区设置。

至此,完成了这道笔试题。从代码的角度再来总结下这道题考察的知识点:

  1. 如何实现多线程
  2. 多线程场景如何保证临界资源的安全性
  3. 多线程同步工具类CountDownLatch的使用场景

至此,对这道笔试题总结完毕。

由于笔主水平有限,笔误或者不当之处还请批评指正。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值