力扣多线程笔试题(Leetcode 1115、1116、1117、1195)

并发编程

本章笔试题需要用到的并发编程知识,可以看看我这篇文章 Semaphore、CyclicBarrier和CountDownLatch三者的区别


1115、交替打印FooBar(中等)

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

转载请标注原作者,本篇文章对你有帮助的话就给个赞赞赞赞赞…

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沉淀顶峰相见的PET

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值