LeetCode 多线程练习1(1114. 按序打印 / 1115. 交替打印FooBar)

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode 多线程练习1(1114. 按序打印 / 1115. 交替打印FooBar)相关的知识,希望对你有一定的参考价值。

多线程练习1

1114. 按序打印

题目描述

我们提供了一个类:

public class Foo {
public void first() { print(“first”); }
public void second() { print(“second”); }
public void third() { print(“third”); }
}
三个不同的线程 A、B、C 将会共用一个 Foo 实例。

一个将会调用 first() 方法
一个将会调用 second() 方法
还有一个将会调用 third() 方法
请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

示例 1:

输入: [1,2,3]
输出: “firstsecondthird”
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 second() 方法,线程 C 将会调用 third() 方法。
正确的输出是 “firstsecondthird”。

示例 2:

输入: [1,3,2]
输出: “firstsecondthird”
解释:
输入 [1,3,2] 表示线程 A 将会调用 first() 方法,线程 B 将会调用 third() 方法,线程 C 将会调用 second() 方法。
正确的输出是 “firstsecondthird”。

提示:

尽管输入中的数字似乎暗示了顺序,但是我们并不保证线程在操作系统中的调度顺序。
你看到的输入格式主要是为了确保测试的全面性。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/print-in-order
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

好像就是爱奇艺的原题,完全不知道咋做
力扣真香

并发主要为多任务情况设计。但如果应用不当,可能会引发一些漏洞。按照情况不同,可以分为三种:
竞态条件:由于多进程之间的竞争执行,导致程序未按照期望的顺序输出。
死锁:并发程序等待一些必要资源,导致没有程序可以执行。
资源不足:进程被永久剥夺了运行所需的资源。

推荐题解:https://leetcode-cn.com/problems/print-in-order/solution/chang-you-duo-xian-cheng-zhi-zhi-shun-xu-it6f/

第一种实现方式:原子类

因为第二个等待第一个,第三个等待第二个,可以让第一个方法和第二个方法共享一个变量firstJobDone,第二个方法和第三个共享一个变量secondJobDone
首先初始化两个变量,第一个方法正常执行,执行完以后更新变量firstJobDone,然后在第二个方法中检查firstJobDone的状态,如果不等于1就等待,如果等于1就执行,同时更新secondJobDone
第三个方法执行过程同第二个方法

class Foo {
    //第一种方式,原子类的整数
    private AtomicInteger firstJobDone = new AtomicInteger(0);      //第一个任务完成标志
    private AtomicInteger secondJobDone = new AtomicInteger(0);     //第二个任务完成标志


    public Foo() {
        
    }

    public void first(Runnable printFirst) throws InterruptedException {
        //第一个任务正常执行
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        firstJobDone.incrementAndGet();     //加一
    }

    public void second(Runnable printSecond) throws InterruptedException {
        //如果第一个任务没完成,就不能执行
        while(firstJobDone.get() != 1){

        }
            // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        secondJobDone.incrementAndGet();    //加一
        
    }

    public void third(Runnable printThird) throws InterruptedException {
        while(secondJobDone.get() != 1){

        }
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}
第二种实现方式:信号量Semaphore

参考:https://blog.csdn.net/zbc1090549839/article/details/53389602
Semaphore是用来保护一个或者多个共享资源的访问,Semaphore内部维护了一个计数器,其值为可以访问的共享资源的个数。一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

就好比一个厕所管理员,站在门口,只有厕所有空位,就开门允许与空侧数量等量的人进入厕所。多个人进入厕所后,相当于N个人来分配使用N个空位。为避免多个人来同时竞争同一个侧卫,在内部仍然使用锁来控制资源的同步访问。

class Foo {
    //第二种方式,信号量
    Semaphore first = new Semaphore(0);
    Semaphore second = new Semaphore(0);

    public Foo() {
        
    }

    public void first(Runnable printFirst) throws InterruptedException {
        //第一个任务正常执行
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        //信号量释放,相当于该资源的个数加一
        first.release();    //加一
    }

    public void second(Runnable printSecond) throws InterruptedException {
        //获取信号量,如果是0,休眠
        first.acquire();
            // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        second.release();    //加一
        
    }

    public void third(Runnable printThird) throws InterruptedException {
        second.acquire();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}
第三种方式:原始的同步原语+synchronized

1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。

2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。

3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。

当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。

只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。

也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程让其获得锁

4、wait() 需要被try catch包围,以便发生异常中断也可以使wait等待的线程唤醒。

5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。

6、notify 和 notifyAll的区别

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。

7、在多线程中要测试某个条件的变化,使用if 还是while?

要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑;显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while进行等待,直到满足条件才继续往下执行

class Foo {
    //第三种方式:原始的同步原语+synchronized
    private volatile int flag = 1;
    private final Object lock = new Object();

    public Foo() {
        
    }

    public void first(Runnable printFirst) throws InterruptedException {
        //第一个任务正常执行
        synchronized(lock){
            while(flag != 1){
                lock.wait();
            }
            // printFirst.run() outputs "first". Do not change or remove this line.
            printFirst.run();
            flag = 2;
            lock.notifyAll();
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        synchronized(lock){
            while(flag != 2){
                lock.wait();
            }
            // printSecond.run() outputs "second". Do not change or remove this line.
            printSecond.run();
            flag = 3;
            lock.notifyAll();
        }   
    }

    public void third(Runnable printThird) throws InterruptedException {
        synchronized(lock){
            while(flag != 3) lock.wait();
            // printThird.run() outputs "third". Do not change or remove this line.
            printThird.run();
            flag = 1;
            lock.notifyAll();
        }   
    }
}
第四种方式:CountDownLatch

CountDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。

是通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在闭锁上等待的线程就可以恢复工作了

//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException { };   
//和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  
//将count值减1
public void countDown() { };  
class Foo {
    //方法4:CountDownLatch
    CountDownLatch latch1 = new CountDownLatch(1);
    CountDownLatch latch2 = new CountDownLatch(1);

    public Foo() {
        
    }

    public void first(Runnable printFirst) throws InterruptedException {
        
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        //first执行完,将latch1减1,即为0,表示可以执行了
        latch1.countDown();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        //如果latch1不等于0,那么就等待
        latch1.await();
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        latch2.countDown();
    }

    public void third(Runnable printThird) throws InterruptedException {
        latch2.await();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}
第五种方法:SynchronousQueue

SynchronousQueue 是一个队列来的,但它的特别之处在于它内部没有容器,一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(当然也可以先take后put,原理是一样的)

class Foo {
    //阻塞队列         
    //同步队列,没有容量,进去一个元素,必须等待取出来以后,才能再往里面放一个元素
    BlockingQueue<Integer> block1 = new SynchronousQueue<>();
    BlockingQueue<Integer> block2 = new SynchronousQueue<>();

    public Foo() {
        
    }

    public void first(Runnable printFirst) throws InterruptedException {
        
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        block1.put(1);
    }

    public void second(Runnable printSecond) throws InterruptedException {
        block1.take();
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        block2.put(1);
    }

    public void third(Runnable printThird) throws InterruptedException {
        block2.take();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}
第六种方式:ReentrantLock+Condition

Condition可以通俗的理解为条件队列。当一个线程在调用了await方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition的实例必须与一个Lock绑定,因此Condition一般都是作为Lock的内部实现。

await() :造成当前线程在接到信号或被中断之前一直处于等待状态。
await(long time, TimeUnit unit) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。
awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在nanosTimesout之前唤醒,那么返回值 = nanosTimeout - 消耗时间,如果返回值 <= 0 ,则可以认定它已经超时了。
awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。【注意:该方法对中断不敏感】。
awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true,否则表示到了指定时间,返回返回false。
signal() :唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition相关的锁。
signalAll() :唤醒所有等待线程。能够从等待方法返回的线程必须获得与Condition相关的锁。

class Foo {
    int num;
    Lock lock;
    //精确的通知和唤醒线程
    Condition condition1, condition2, condition3;

    public Foo() {
        num = 1;
        lock = new ReentrantLock();
        condition1 = lock.newCondition();
        condition2 = lock.newCondition();
        condition3 = lock.newCondition();
    }

    public void first(Runnable printFirst) throws InterruptedException {
        lock.lock();
        try{
            while(num != 1){
                condition1.await();
            }
            // printFirst.run() outputs "first". Do not change or remove this line.
            printFirst.run();
            num = 2;
            condition2.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        lock.lock();
        try{
            while(num != 2){
                condition2.await();
            }
            // printSecond.run() outputs "second". Do not change or remove this line.
            printSecond.run();
            num = 3;
            condition3.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        lock.lock();
        try{
            while(num != 3){
                condition3.await();
            }
            // printThird.run() outputs "third". Do not change or remove this line.
            printThird.run();
            num = 1;
            condition1.signal();
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
}
第七种方法:LockSupport

LockSupport类的核心方法其实就两个:park()和unpark(),其中park()方法用来阻塞当前调用线程,unpark()方法用于唤醒指定线程。
这其实和Object类的wait()和signal()方法有些类似,但是LockSupport的这两种方法从语意上讲比Object类的方法更清晰,而且可以针对指定线程进行阻塞和唤醒。

LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1。
初始时,permit为0,当调用unpark()方法时,线程的permit加1,当调用park()方法时,如果permit为0,则调用线程进入阻塞状态。

没看懂这个代码:

class Foo {
    //这个没看懂
    private AtomicInteger counter = new AtomicInteger(0);
    private Map<String, Thread> threads = new HashMap<>();
    
    public Foo() {

    }

    public void first(Runnable printFirst) throws InterruptedException {
        while (counter.get() != 0) {
            threads.put("first", Thread.currentThread());
            LockSupport.park();
        }
        printFirst.run();
        counter.getAndIncrement();
        threads.forEach((k, v) -> LockSupport.unpark(v));
    }

    public void second(Runnable printSecond) throws InterruptedException {
        while (counter.get() != 1) {
            threads.put("second",Thread.currentThread());
            LockSupport.park();
        }
        printSecond.run();
        counter.getAndIncrement();
        threads.forEach((k, v) -> LockSupport.unpark(v));
    }

    public void third(Runnable printThird) throws InterruptedException {
        while (counter.get() != 2) {
            threads.put("third",Thread.currentThread());
            LockSupport.park();
        }
        printThird.run();
        counter.getAndIncrement();
        threads.forEach((k, v) -> LockSupport.unpark(v));
    }
}

1115. 交替打印FooBar

题目描述

我们提供一个类:

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。

请设计修改程序,以确保 “foobar” 被输出 n 次。

示例 1:

输入: n = 1
输出: “foobar”
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,“foobar” 将被输出一次。

示例 2:

输入: n = 2
输出: “foobarfoobar”
解释: “foobar” 将被输出两次。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/print-foobar-alternately
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

有了昨天的练习,感觉还是能写:
然后用原子类写了下面的代码:
测试输入多少都能过,但是一提交,5就超时了
可能是自旋耗时间?

原子类(超时)
class FooBar {
    private int n;

以上是关于LeetCode 多线程练习1(1114. 按序打印 / 1115. 交替打印FooBar)的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode(多线程)- 1114. 按序打印

LeetCode1114. 按序打印[多线程] 6种解法

LeetCode 多线程 1114. 按序打印

(LeetCode)1114. 按序打印

LeetCode刷题 多线程编程九则 | 1188. 设计有限阻塞队列 1242. 多线程网页爬虫 1279. 红绿灯路口

LeetCode(多线程)- 题集