并发编程
本章笔试题需要用到的并发编程知识,可以看看我这篇文章 Semaphore、CyclicBarrier和CountDownLatch三者的区别
目录
1115、交替打印FooBar(中等)
CyclicBarrier解答
我们先来看看题目,交替打印且输出n次,表示需要循环打印n次,首先想到的就是CyclicBarrier,它可以循环调用多次,我们直接来上代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
//定义一个屏障,要通过这个屏障的条件是调用2次await()
CyclicBarrier cb = new CyclicBarrier(2);
volatile boolean bool = true;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
//双重判断
while(!bool);
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
bool = false;
//打印完foo后,执行await()表示foo已打印
try{
cb.await();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
try{
//当我们再一次执行此方法时,printBar.run()才能执行
//因为这个放在printBar.run()后边,可能会导致bar打印在foo前面
cb.await();
}catch(BrokenBarrierException e){
e.printStackTrace();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
bool = true;
}
}
}
思路:
1.首先我们先定义一个屏障类,这个屏障类需要调用2次await()
来实现打开屏障
2.还需要定义一个boolean的变量,初始值为true,用来控制整个流程的执行,使用volatile修饰是因为在高并发情况下,这个bool变量需要对每个线程可见,也就是当1个线程修改了这个bool变量,会立即刷新到缓存中,供别的线程可见
3.线程A执行 foo(Runnable printFoo) 方法,首先我们判断bool类型的数据是否符合要求,如果不符合就一直处于while循环中,不会await()
,第1次进入因为bool为true,就会跳出while(!bool),执行下边的语句,打印foo,将bool设置为false,然后重点来了,执行await()
,表示第1个条件已完成,但是我需要2个才可以往下走,线程阻塞在这
4.线程B执行 bar(Runnable printBar) 方法,直接执行await()
,这里要注意了,两次await()
已经打开了屏障,线程A、线程B都可以往下执行,如果此时线程A没有使用一个bool来阻塞线程,那么当线程B延时执行了一小会,线程A可能已经打印了多次foo字段,这并不符合我们的要求,这就是使用while(!bool)的目的;线程B继续往下执行,打印bar,将bool设置为true,当bool为true时,线程A又可以继续往下执行了,通过两个条件的限制就完成了解答
Semaphore解答
这题使用信号量来解决也是可以的,限制资源的个数,竞争到资源的线程才可以往下执行,我们直接上代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
Semaphore foo = new Semaphore(1);
Semaphore bar = new Semaphore(0);
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
foo.acquire();
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
bar.release();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
bar.acquire();
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
foo.release();
}
}
}
思路:
1.这里我们定义了两个信号量,foo和bar,但是只设置了一个资源
2.首先线程A调用foo.acquire()
来得到资源,可以往下面执行,而线程B调用bar.acquire()
并没有获取到资源,所以线程B阻塞在这行代码,当线程A执行完printFoo.run()
后,调用bar.release()
来释放资源,表示下一次资源的获取是通过bar.acquire()
,所以调用bar.acquire()
的线程B就可以执行下边的代码
3.当线程B执行完printBar.run()
后,又调用foo.release()
来释放资源,线程A又调用foo.acquire()
来得到资源,执行下边的代码,如此反复得到交替打印n次的效果
无锁模式(在Leetcode中测试超时)
咱们直接上代码
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
volatile boolean bool = true;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; ) {
while(bool){
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
i += 1;
bool = false;
}
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n;) {
while(!bool){
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
i += 1;
bool = true;
}
}
}
}
思路:
这道题比较特殊,因为只有两种情况,完全可以使用boolean来判断哪个该打印,就目前位置效率还是最高的,但使用前面两种锁的方案还是比较安全的
1116、打印零与奇偶数(中等)
1116.打印零与奇偶数
我们先来看看问题,还是要保证输出顺序,满足条件的输出,像这种题,我们拿Semaphore一般都可以做,也就是只有一个资源,三个线程争抢,在这里我们就得设置条件来让符合条件的线程去争抢到资源了,直接上代码
Semaphore
class ZeroEvenOdd {
private int n;
public ZeroEvenOdd(int n) {
this.n = n;
}
Semaphore zero = new Semaphore(1);
Semaphore even = new Semaphore(0);
Semaphore odd = new Semaphore(0);
// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
for(int i = 1;i<=n;i++){
zero.acquire();
printNumber.accept(0);
if(i % 2 == 1){
odd.release();
}else{
even.release();
}
}
}
public void even(IntConsumer printNumber) throws InterruptedException {
for(int i = 2;i<=n;i+=2){
even.acquire();
printNumber.accept(i);
zero.release();
}
}
public void odd(IntConsumer printNumber) throws InterruptedException {
for(int i = 1;i<=n;i+=2){
odd.acquire();
printNumber.accept(i);
zero.release();
}
}
}
思路:
1.首先这三个方法是分别交予三个线程执行,为了保证执行顺序,定义三个信号量(zero、odd、even),分别对应三个线程,只有一个资源
2.大家看看输出效果,每打印一个奇数或者偶数前面都会带一个0,这个0就是我们解题的关键,我们可以在**zero(IntConsumer printNumber)**这个方法里设置一个for循环,每一次循环都先让信号量zero获取资源,打印0,然后判断当前循环的i
(循环到的数字),是奇数还是偶数,如果是奇数则调用odd信号量的odd.release()
,偶数则调用even.release()
来获取资源,另外两个线程就能响应acquire()
来得到资源执行下面的方法
3.打印奇数与偶数的方法里也要定义for循环体,奇数的i
从1开始,偶数的i
从2开始,每次循环结束都加2,这样我们分配资源的职责都在zero方法中,奇数与偶数的方法只需要获取资源打印数字即可
4.每次循环结束都需要调用zero.release()
来回到zero方法中继续判断数字的奇偶,循环往复就能完成我们的要求了
1117、H2O 生成(中等)
1117.H2O 生成
这道题也是一样,需要我们将输入的H与O转换为水分子的形式,就是说三个为一组,HH后面必须跟O,O后面必须有两个H,又或者可以是HOH这样的形式,又是一道典型的信号量题目,我们使用信号量来控制线程打印H与O的次数即可
class H2O {
public H2O() {
}
Semaphore Hs = new Semaphore(2);
Semaphore Os = new Semaphore(0);
public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
Hs.acquire();
// releaseHydrogen.run() outputs "H". Do not change or remove this line.
releaseHydrogen.run();
Os.release();
}
public void oxygen(Runnable releaseOxygen) throws InterruptedException {
Os.acquire(2);
// releaseOxygen.run() outputs "O". Do not change or remove this line.
releaseOxygen.run();
Hs.release(2);
}
}
思路:
1.首先清楚,打印字母H与字母O的方法是有两个线程执行的,这样的话我们就定义两个信号量(Hs、Os)
2.Hs信号量拥有两个资源,Os先不分配资源
3.线程A调用Hs.acquire()
获取一个资源来执行releaseOxygen.run()
(打印H字符的方法),然后调用Os.release()
来释放一个资源,然后注意,线程B并不是调用Os.acquire()
来获取资源了,而是Os.acquire(2)
,也就是说,线程B得获取到两个资源才可以执行releaseOxygen.run()
(打印O字符的方法)这里是一个重点
4.线程A又可以继续执行Hs.acquire()
获取一个资源来打印H字符,然后调用Os.release()
来释放一个资源,这样线程B就可以调用Os.acquire(2)
来获取到两个资源执行打印字符O的操作了
5.最后线程B调用Hs.release(2)
来释放两个资源即可,线程A又可以获取资源循环往复执行下去
ReentrantLock + Condition
使用显示锁的好处:
尝试非阻塞的获取锁、获取锁的过程中可以被中断、超时可以放弃获取锁不会阻塞住线程
我们先上代码,后面讲思路
class H2O {
public H2O() {
}
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//记录H的打印数
volatile int count = 0;
public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
//首先尝试获取锁,而且必须在finally块中释放锁
lock.lock();
try{
while(count == 2){
//调用await()阻塞线程
condition.await();
}
// releaseHydrogen.run() outputs "H". Do not change or remove this line.
releaseHydrogen.run();
count++;
condition.signalAll();
}finally{
lock.unlock();
}
}
public void oxygen(Runnable releaseOxygen) throws InterruptedException {
lock.lock();
try{
while(count < 2){
condition.await();
}
// releaseOxygen.run() outputs "O". Do not change or remove this line.
releaseOxygen.run();
count = 0;
condition.signalAll();
}finally{
lock.unlock();
}
// releaseOxygen.run() outputs "O". Do not change or remove this line.
}
}
思路:
1.我先来解释一下这个condition.await()
与condition.signalAll()
2.condition.await()
是将线程阻塞,然后释放锁资源给别的线程获取;condition.signalAll()
是将等待队列中的线程唤醒
3.我们来走一遍流程,首先两个线程去争取锁资源
假设一:线程B也就是oxygen(Runnable releaseOxygen)
方法的线程获取到锁资源,进到方法中,因为count < 2
,线程B调用condition.await()
方法,释放所资源,阻塞线程B
这时线程A也就是hydrogen(Runnable releaseHydrogen)
得到锁资源,执行方法,因为count != 2
不用阻塞线程,继续往下执行,打印一个H字符,count加1,调用condition.signalAll()
唤醒等待队列的所有进程,这时又是线程B获取到锁,但是count的值为1也就是count < 2
,继续阻塞,释放锁资源,走线程A的流程,走完第二轮了之后,到这count == 2
就成立了,就轮到线程B执行了,打印O字符,设置count = 0
,唤醒等待线程继续下面的流程
假设二:线程A得到锁资源,就和上面的一样,只是比假设一少了线程B一开始释放锁资源的那一步骤
通过线程的不断竞争锁资源来控制线程的并发,也能完成我们的目标
1195、交替打印字符串(中等)
1195.交替打印字符串
这道题和前面那个1115、交替打印Foobar类似,只不过这个更为复杂,但其实原理还是一样的
Semaphore
直接上代码给大家讲解
class FizzBuzz {
private int n;
Semaphore s1 = new Semaphore(0);
Semaphore s2 = new Semaphore(0);
Semaphore s3 = new Semaphore(0);
Semaphore s4 = new Semaphore(1);
public FizzBuzz(int n) {
this.n = n;
}
// printFizz.run() outputs "fizz".
public void fizz(Runnable printFizz) throws InterruptedException {
for(int i = 1;i<=n;i++){
if(i % 3 == 0 && i % 5 != 0){
s3.acquire();
printFizz.run();
s4.release();
}
}
}
// printBuzz.run() outputs "buzz".
public void buzz(Runnable printBuzz) throws InterruptedException {
for(int i = 1;i<=n;i++){
if(i % 5 == 0 && i % 3 != 0){
s2.acquire();
printBuzz.run();
s4.release();
}
}
}
// printFizzBuzz.run() outputs "fizzbuzz".
public void fizzbuzz(Runnable printFizzBuzz) throws InterruptedException {
for(int i = 1;i<=n;i++){
if(i % 5 == 0 && i % 3 == 0){
s1.acquire();
printFizzBuzz.run();
s4.release();
}
}
}
// printNumber.accept(x) outputs "x", where x is an integer.
public void number(IntConsumer printNumber) throws InterruptedException {
s4.acquire();
for(int i = 1;i<=n;i++){
if(i % 3 == 0 && i % 5 ==0){
s1.release();
}else if(i % 5 == 0 && i % 3 != 0){
s2.release();
}else if(i % 3 == 0 && i % 5 != 0){
s3.release();
}else{
printNumber.accept(i);
}
}
}
}
思路:
1.这里4个线程来分别控制四个方法,我们还是定义4个信号量且只用一个资源来控制它们的执行顺序
2.获取到资源的才能执行方法,我们在number()
里判断轮询到的数据该是打印那种字符串,这里使用了if-else
来做判断,具体的我就不解释了,总的来说就是满足条件的我就调用对应的release()
来释放资源,然后对应线程就会直接响应得到资源执行方法体
总结
本篇文章主要总结了4道经典的并发编程笔试题,这种类型的题目能快速的帮助我们掌握并发编程工具类基本使用
完成:2021/4/09 17:00 ALiangX
转载请标注原作者,本篇文章对你有帮助的话就给个赞赞赞赞赞…