LeetCode——多线程问题汇总

Posted xym4869

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LeetCode——多线程问题汇总相关的知识,希望对你有一定的参考价值。


一般解决多线程问题,有以下几种解决方式:

  1. Semaphore(信号量)
  2. Lock(管程模型),无锁
  3. CountDownLatch(计数器)
  4. CyclicBarrier(循环栅栏)

Semaphore(信号量)

Semaphore主要用于控制当前活动线程数目,就如同停车场系统一般,而Semaphore则相当于看守的人,用于控制总共允许停车的停车位的个数,而对于每辆车来说就如同一个线程,线程需要通过acquire()方法获取许可,而release()释放许可。如果许可数达到最大活动数,那么调用acquire()之后,便进入等待队列,等待已获得许可的线程释放许可,从而使得多线程能够合理的运行。

以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。这个停车系统中,每辆车就好比一个线程,看门人就好比一个信号量,看门人限制了可以活动的线程。假如里面依然是三个车位,但是看门人改变了规则,要求每次只能停两辆车,那么一开始进入两辆车,后面得等到有车离开才能有车进入,但是得保证最多停两辆车。对于Semaphore类而言,就如同一个看门人,限制了可活动的线程数。
主要方法
Semaphore(int permits):构造方法,创建具有给定许可数的计数信号量并设置为非公平信号量。
Semaphore(int permits,boolean fair):构造方法,当fair等于true时,创建具有给定许可数的计数信号量并设置为公平信号量。
void acquire():从此信号量获取一个许可前线程将一直阻塞。相当于一辆车占了一个车位。
void acquire(int n):从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。比如n=2,就相当于一辆车占了两个车位。
void release():释放一个许可,将其返回给信号量。就如同车开走返回一个车位。
void release(int n):释放n个许可。
int?availablePermits():当前可用的许可数。

Lock(管程模型)

Condition实现了管程模型里面的条件变量。Synchronized只有一个条件变量,而Lock&Condition实现的管程是支持多个条件变量的。
线程之间的交互,主要通过两个函数完成,Object.wait()和Object.notify(),这两个函数搭配synchronized关键字使用。而Condition有着大致相同的功能,它与重入锁相关联,因此Condition一般都是作为Lock的内部实现。
Condition接口提供的基本方法:

public interface Condition {
    void await() throws InterruptedException;//使线程等待,同时释放当前锁
    void awaitUninterruptibly();//与await()方法基本相同,但它不会在等待过程中响应中断
    long awaitNanos(long var1) throws InterruptedException;
    boolean await(long var1, TimeUnit var3) throws InterruptedException;
    boolean awaitUntil(Date var1) throws InterruptedException;
    void signal();//用于唤醒一个在等待中的线程
    void signalAll();//会唤醒所有在等待的线程
}

和Object.wait()和Object.notify()一样,当线程执行condition.await()方法,线程必须已经获得了该重入锁(否则都不占用锁,哪来的释放锁让别的线程占用);同样,线程在执行condition.signal()时,系统会在等待队列中唤醒一个线程,线程一旦唤醒,它会重新尝试获得与之相关联的锁,一旦成功则继续执行。因此,执行signal()方法后需要释放锁。
技术图片
Condition 也行 wait 也好,套路就是使用三个工具来完成三步套路。即,用两个线程,同时跑两个代码,并且用 while 不段的去读取一个条件,来判断自己是否应该唤醒对方。

步骤:

  1. 先lock住
  2. 通过 lock 拿到 condition。再进行操作如 await
  3. 然后多个线程开始 await、single

注意 await 会释放锁。await()的作用是能够让其他线程访问竞争资源,所以挂起状态就是要释放竞争资源的锁。当前线程加入Condition对象维护的等待队列中,当其他线程中使用signal()或signalAll()方法时,线程会重新获得锁继续执行。或当线程被中断时,也会跳出等待。

写这部分比较好的可以看这篇:Java多线程高并发学习笔记(二)——深入理解ReentrantLock与Condition

无锁(可以用有锁的也可以用无锁解决)

对于并发控制而言,锁是一种悲观的策略。它总是假设每一次的临界区操作会产生冲突,因此,必须对每次操作都小心翼翼。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,所以说锁会阻塞线程执行。
而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的。既然没有冲突,自然不需要等待,所以所有的线程都可以在不停顿的状态下持续执行。那遇到冲突怎么办呢?无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

无锁的好处:
第一,在高并发的情况下,它比有锁的程序拥有更好的性能;
第二,它天生就是死锁免疫的。
就凭借这两个优势,就值得我们冒险尝试使用无锁的并发。

CAS算法的过程是这样:它包含三个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
简单地说,CAS需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,那说明它已经被别人修改过了。你就重新读取,再次尝试修改就好了。

CountDownLatch(计数器)

如果使用的场景为每个方法只会调用一次,那么适合用CountDownLatch(计数器)

CountDownLatch同步计数器,当计数器数值减为0时,所有受其影响而等待的线程将会被激活,这样保证模拟并发请求的真实性。
CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。

你可以向CountDownLatch对象设置一个初始的数字作为计数值,任何调用这个对象上的await()方法都会阻塞,直到这个计数器的计数值被其他的线程减为0为止。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。

典型用法
CountDownLatch典型用法:1、某一线程在开始运行前等待n个线程执行完毕。将CountDownLatch的计数器初始化为new CountDownLatch(n),每当一个任务线程执行完毕,就将计数器减1 countdownLatch.countDown(),当计数器的值变为0时,在CountDownLatch上await()的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
CountDownLatch典型用法:2、实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的CountDownLatch(1),将其计算器初始化为1,多个线程在开始执行任务前首先countdownlatch.await(),当主线程调用countDown()时,计数器变为0,多个线程同时被唤醒。

CountDownLatch的不足
CountDownLatch是一次性的,计算器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当CountDownLatch使用完毕后,它不能再次被使用。

方法说明
public void countDown()
  递减锁存器的计数,如果计数到达零,则释放所有等待的线程。如果当前计数大于零,则将计数减少.

public boolean await(long timeout,TimeUnit unit) throws InterruptedException
  使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断或超出了指定的等待时间。如果当前计数为零,则此方法立刻返回true值。
  如果当前计数大于零,则出于线程调度目的,将禁用当前线程,且在发生以下三种情况之一前,该线程将一直出于休眠状态:由于调用countDown()方法,计数到达零;或者其他某个线程中断当前线程;或者已超出指定的等待时间。

  • 如果计数到达零,则该方法返回true值。
  • 如果当前线程,在进入此方法时已经设置了该线程的中断状态;或者在等待时被中断,则抛出InterruptedException,并且清除当前线程的已中断状态。
  • 如果超出了指定的等待时间,则返回值为false。如果该时间小于等于零,则该方法根本不会等待。

参数:
  timeout-要等待的最长时间
  unit-timeout 参数的时间单位
返回:
  如果计数到达零,则返回true;如果在计数到达零之前超过了等待时间,则返回false
抛出:
  InterruptedException-如果当前线程在等待时被中断

CyclicBarrier(循环栅栏)

如果使用的场景为有循环,那么适合使用CyclicBarrier。
从字面上的意思可以知道,这个类的中文意思是循环栅栏。大概的意思就是一个可循环利用的屏障。它的作用就是会让所有线程都等待完成后才会继续下一步行动
举个例子,就像生活中我们会约朋友们到某个餐厅一起吃饭,有些朋友可能会早到,有些朋友可能会晚到,但是这个餐厅规定必须等到所有人到齐之后才会让我们进去。这里的朋友们就是各个线程,餐厅就是 CyclicBarrier。
栅栏类似于闭锁,它能阻塞一组线程直到某个事件的发生。栅栏与闭锁的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。闭锁用于等待事件,而栅栏用于等待其他线程。
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将被重置以便下次使用。

方法

public CyclicBarrier(int parties) //parties 是参与线程的个数
public CyclicBarrier(int parties, Runnable barrierAction) //Runnable 参数的意思是最后一个到达线程要做的任务

public int await() throws InterruptedException, BrokenBarrierException //await() 表示自己已经到达栅栏,BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException

CyclicBarrier可以用于多线程计算数据,最后合并计算结果的场景。
CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程使用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。调用await方法的线程告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞,直到parties个参与线程调用了await方法。默认barrier是没有损坏的。当barrier损坏了或者有一个线程中断了,则通过breakBarrier()来终止所有的线程。

CyclicBarrier 与 CountDownLatch 区别

  • CountDownLatch 是一次性的,CyclicBarrier 是可循环利用的
  • CountDownLatch 参与的线程的职责是不一样的,有的在倒计时,有的在等待倒计时结束。CyclicBarrier 参与的线程职责是一样的。
  • CountDownLatch 基于 AQS 的共享模式的使用,而 CyclicBarrier 基于 Condition 来实现的。

这两个类都可以实现一组线程在到达某个条件之前进行等待,它们内部都有一个计数器,当计数器的值不断的减为0的时候所有阻塞的线程将会被唤醒。
有区别的是CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。
另外,CountDownLatch只能拦截一轮,而CyclicBarrier可以实现循环拦截。一般来说用CyclicBarrier可以实现CountDownLatch的功能,而反之则不能,例如上面的赛马程序就只能使用CyclicBarrier来实现。总之,这两个类的异同点大致如此,至于何时使用CyclicBarrier,何时使用CountDownLatch,还需要读者自己去拿捏。

题目举例

把并发调用变成按顺序调用

按序打印

我们提供了一个类:

public class Foo {
? public void one() { print("one"); }
? public void two() { print("two"); }
? public void three() { print("three"); }
}

三个不同的线程将会共用一个?Foo?实例。
线程 A 将会调用 one() 方法
线程 B 将会调用?two() 方法
线程 C 将会调用 three() 方法
请设计修改程序,以确保 two() 方法在 one() 方法之后被执行,three() 方法在 two() 方法之后被执行。

示例 1:
输入: [1,2,3]
输出: "onetwothree"
解释:
有三个线程会被异步启动。
输入 [1,2,3] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 two() 方法,线程 C 将会调用 three() 方法。
正确的输出是 "onetwothree"。
示例 2:
输入: [1,3,2]
输出: "onetwothree"
解释:
输入 [1,3,2] 表示线程 A 将会调用 one() 方法,线程 B 将会调用 three() 方法,线程 C 将会调用 two() 方法。
正确的输出是 "onetwothree"。

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

A:
该题为典型的“每个方法只会被调用一次”题目。

main函数:

public static void main(String[] args) throws InterruptedException{
        Foo foo = new Foo();
        Thread t1 = new Thread(()->{
            try {
                foo.first(()-> System.out.print("one"));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(()->{
            try {
                foo.second(()-> System.out.print("two"));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });
        Thread t3 = new Thread(()->{
            try {
                foo.third(()-> System.out.print("three"));
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        });

        t1.start();
        t2.start();
        t3.start();
    }

1.CountDownLatch

class Foo {

    public Foo() {
        
    }
    CountDownLatch stage1 = new CountDownLatch(1);
    CountDownLatch stage2 = new CountDownLatch(1);

    public void first(Runnable printFirst) throws InterruptedException {
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        stage1.countDown();//stage1结束
    }

    public void second(Runnable printSecond) throws InterruptedException {
        stage1.await();//stage1阻塞等待,结束了就可以执行下面的run了
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        stage2.countDown();//stage2结束
    }

    public void third(Runnable printThird) throws InterruptedException {
        stage2.await();//stage2阻塞等待
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}

2.Semaphore

class Foo {

    public Foo() {
        
    }
    Semaphore stage1 = new Semaphore(1);//只有stage1允许开始,如果将给定许可数设置为1,就如同一个单例模式
    Semaphore stage2 = new Semaphore(0);
    Semaphore stage3 = new Semaphore(0);
    public void first(Runnable printFirst) throws InterruptedException {
        stage1.acquire();//stage1获得许可,可以run
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        stage2.release();//释放stage2,允许stage2 run,即下一个走stage2
    }

    public void second(Runnable printSecond) throws InterruptedException {
        stage2.acquire();
        // printSecond.run() outputs "second". Do not change or remove this line.
        printSecond.run();
        stage3.release();
    }

    public void third(Runnable printThird) throws InterruptedException {
        stage3.acquire();
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}

3.Lock:管程模型是传说中的万能模型,不过实践发现,代码量比较臃肿

class Foo {
    Lock lock = new ReentrantLock();
    Condition stage2 = lock.newCondition();
    Condition stage3 = lock.newCondition();
    volatile int stage = 1;

    public Foo() {

    }

    public void first(Runnable printFirst) throws InterruptedException {
        lock.lock();//先加锁,再拿condition
        try {
            // printFirst.run() outputs "first". Do not change or remove this line.
            printFirst.run();
            stage = 2;
            stage2.signal();//唤醒stage2
        } finally {
            lock.unlock();//lock了一定要记得在finally中unlock!!!
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        lock.lock();
        try {
            while (stage != 2) {
                stage2.await();//使stage2等待,同时释放当前锁
            }
            // printSecond.run() outputs "second". Do not change or remove this line.
            printSecond.run();
            stage = 3;
            stage3.signal();
        } finally {
            lock.unlock();
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        lock.lock();
        try {
            while (stage != 3) {
                stage3.await();
            }
            // printThird.run() outputs "third". Do not change or remove this line.
            printThird.run();
        } finally {
            lock.unlock();
        }
    }
}

4.无锁:既然有了带锁实现,那么也可以尝试一下无锁实现(实践证明,无锁的效率更高,代码更简洁)

class Foo {
    volatile int stage = 1;

    public Foo() {
        
    }

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

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

    public void third(Runnable printThird) throws InterruptedException {
        while(stage != 3);
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}

5.CyclicBarrier

class Foo {
    CyclicBarrier c1 = new CyclicBarrier(2);
    CyclicBarrier c2 = new CyclicBarrier(2);

    public Foo() {

    }

    public void first(Runnable printFirst) throws InterruptedException {
        // printFirst.run() outputs "first". Do not change or remove this line.
        printFirst.run();
        try {
            c1.await();
        }catch (BrokenBarrierException e){
            e.printStackTrace();
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        try {
            c1.await();
            // printSecond.run() outputs "second". Do not change or remove this line.
            printSecond.run();
            c2.await();
        }catch (BrokenBarrierException e){
            e.printStackTrace();
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        try {
            c2.await();
        }catch (BrokenBarrierException e){
            e.printStackTrace();
        }
        // printThird.run() outputs "third". Do not change or remove this line.
        printThird.run();
    }
}

把并发调用变成交替调用

交替打印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" 将被输出两次。

A:
这个题用lock或无锁都发生“超时”问题,但本地平台无问题。
main函数:

public static void main(String[] args) throws InterruptedException {
        FooBar fooBar = new FooBar(5);

        Thread t1 = new Thread(() -> {
            try {
                fooBar.foo(() -> System.out.print("foo"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread t2 = new Thread(() -> {
            try {
                fooBar.bar(() -> System.out.print("bar"));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
    }

1.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();
        }
    }
}

2.lock

class FooBar {
    private int n;
    Lock lock = new ReentrantLock(true);
    volatile boolean permitFoo = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; ) {
            lock.lock();
            try {
                if (permitFoo) {
                    // printFoo.run() outputs "foo". Do not change or remove this line.
                    printFoo.run();
                    i++;
                    permitFoo = false;
                }
            } finally {
                lock.unlock();
            }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; ) {
            lock.lock();
            try {
                if (!permitFoo) {
                    // printBar.run() outputs "bar". Do not change or remove this line.
                    printBar.run();
                    i++;
                    permitFoo = true;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

3.无锁

class FooBar {
    private int n;

    public FooBar(int n) {
        this.n = n;
    }

    volatile boolean permitFoo = true;

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; ) {
            if(permitFoo) {
                printFoo.run();
                i++;
                permitFoo = false;
            }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; ) {
            if(!permitFoo) {
                printBar.run();
                i++;
                permitFoo = true;
            }
        }
    }
}

4.CyclicBarrier

class FooBar {
    private int n;
    CyclicBarrier cb = new CyclicBarrier(2);
    volatile boolean fin = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (!fin) ;
            // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            fin = false;
            try {
                cb.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            try {
                cb.await();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            // printBar.run() outputs "bar". Do not change or remove this line.
            printBar.run();
            fin = true;
        }
    }
}

把并发调用变成按特定规律顺序调用

打印零与奇偶数

假设有这么一个类:

class ZeroEvenOdd {
? public ZeroEvenOdd(int n) { ... }?     // 构造函数
  public void zero(printNumber) { ... }  // 仅打印出 0
  public void even(printNumber) { ... }  // 仅打印出 偶数
  public void odd(printNumber) { ... }   // 仅打印出 奇数
}

相同的一个?ZeroEvenOdd?类实例将会传递给三个不同的线程:
线程 A 将调用?zero(),它只输出 0 。
线程 B 将调用?even(),它只输出偶数。
线程 C 将调用?odd(),它只输出奇数。
每个线程都有一个?printNumber 方法来输出一个整数。请修改给出的代码以输出整数序列?010203040506... ,其中序列的长度必须为 2n。

示例 1:
输入:n = 2
输出:"0102"
说明:三条线程异步执行,其中一个调用 zero(),另一个线程调用 even(),最后一个线程调用odd()。正确的输出为 "0102"。
示例 2:
输入:n = 5
输出:"0102030405"

A:
用lock写起来比较麻烦,但可以测试成功;用无锁,本地可以跑通,但是会超时。
1.Semaphore

class ZeroEvenOdd {
    private int n;

    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    Semaphore z = new Semaphore(1);
    Semaphore e = new Semaphore(0);
    Semaphore o = new Semaphore(0);

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i = 0; i < n; i++) {//注意i的终结条件
            z.acquire();
            printNumber.accept(0);
            if ((i & 1) == 0) {
                o.release();
            } else {
                e.release();
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i = 2; i <= n; i += 2) {
            e.acquire();
            printNumber.accept(i);
            z.release();
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i = 1; i <= n; i += 2) {
            o.acquire();
            printNumber.accept(i);
            z.release();
        }
    }
}

2.Lock

class ZeroEvenOdd {
    private int n;

    Lock lock = new ReentrantLock();
    Condition z = lock.newCondition();
    Condition num = lock.newCondition();
    volatile boolean zTurn = true;
    volatile int zIndex = 0;

    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (; zIndex < n; ) {
            lock.lock();
            try {
                while (!zTurn) {
                    z.await();
                }
                printNumber.accept(0);
                zTurn = false;
                num.signalAll();
                zIndex++;
            } finally {
                lock.unlock();
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i = 2; i <= n; i += 2) {
            lock.lock();
            try {
                while (zTurn || (zIndex & 1) == 1) {
                    num.await();
                }
                printNumber.accept(i);
                zTurn = true;
                z.signal();
            } finally {
                lock.unlock();
            }
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i = 1; i <= n; i += 2) {
            lock.lock();
            try {
                while (zTurn || (zIndex & 1) == 0) {
                    num.await();
                }
                printNumber.accept(i);
                zTurn = true;
                z.signal();
            } finally {
                lock.unlock();
            }
        }
    }
}

3.无锁

class ZeroEvenOdd {
    private int n;

    public ZeroEvenOdd(int n) {
        this.n = n;
    }

    volatile int stage = 0;

    // printNumber.accept(x) outputs "x", where x is an integer.
    public void zero(IntConsumer printNumber) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (stage > 0) ;
            printNumber.accept(0);
            if ((i & 1) == 0) {
                stage = 1;
            } else {
                stage = 2;
            }
        }
    }

    public void even(IntConsumer printNumber) throws InterruptedException {
        for (int i = 2; i <= n; i += 2) {
            while (stage != 2) ;
            printNumber.accept(i);
            stage = 0;
        }
    }

    public void odd(IntConsumer printNumber) throws InterruptedException {
        for (int i = 1; i <= n; i += 2) {
            while (stage != 1) ;
            printNumber.accept(i);
            stage = 0;
        }
    }
}

把并发调用变成按特定规律顺序调用

这里需要说明的是,场景三与场景四都是都并发调用变成按特定规律顺序调用,但在场景三中,每个方法只会被一个线程调用;而场景四中,每个方法会被多个线程调用。

H2O 生成

现在有两种线程,氢 oxygen 和氧 hydrogen,你的目标是组织这两种线程来产生水分子。
存在一个屏障(barrier)使得每个线程必须等候直到一个完整水分子能够被产生出来。
氢和氧线程会被分别给予 releaseHydrogen 和 releaseOxygen 方法来允许它们突破屏障。
这些线程应该三三成组突破屏障并能立即组合产生一个水分子。

你必须保证产生一个水分子所需线程的结合必须发生在下一个水分子产生之前。
换句话说:
如果一个氧线程到达屏障时没有氢线程到达,它必须等候直到两个氢线程到达。
如果一个氢线程到达屏障时没有其它线程到达,它必须等候直到一个氧线程和另一个氢线程到达。
书写满足这些限制条件的氢、氧线程同步代码。

示例 1:
输入: "HOH"
输出: "HHO"
解释: "HOH" 和 "OHH" 依然都是有效解。
示例 2:
输入: "OOHHHH"
输出: "HHOHHO"
解释: "HOHHHO", "OHHHHO", "HHOHOH", "HOHHOH", "OHHHOH", "HHOOHH", "HOHOHH" 和 "OHHOHH" 依然都是有效解。

限制条件:
输入字符串的总长将会是 3n, 1 ≤?n?≤ 50;
输入字符串中的 “H” 总数将会是 2n;
输入字符串中的 “O” 总数将会是 n。

A:
main函数:

public static void main(String[] args) throws InterruptedException {
        H2O h2O = new H2O();
        for (int i = 0; i < 4; i++) {
            Thread t1 = new Thread(() -> {
                try {
                    h2O.oxygen(() -> {
                        System.out.print(‘H‘);
                    });
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            Thread t2 = new Thread(() -> {
                try {
                    h2O.hydrogen(() -> {
                        System.out.print(‘O‘);
                    });
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t1.start();
            t2.start();
        }
    }

1.Semaphore
h每获取一次释放一个o许可,o每次获取两个许可(即2次h后执行一次o)

class H2O {

    public H2O() {

    }

    Semaphore h = new Semaphore(2);
    Semaphore o = new Semaphore(0);

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        h.acquire();
        // releaseHydrogen.run() outputs "H". Do not change or remove this line.
        releaseHydrogen.run();
        o.release();
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        o.acquire(2);
        // releaseOxygen.run() outputs "O". Do not change or remove this line.
        releaseOxygen.run();
        h.release(2);
    }
}

public class lc124 {
    public static void main(String[] args) throws InterruptedException {
        H2O h2O = new H2O();
        Thread t1 = new Thread(() -> {
            try {
                h2O.oxygen(System.out::println);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread t2 = new Thread(() -> {
            try {
                h2O.hydrogen(System.out::println);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t1.start();
        t2.start();
    }
}

2.lock

class H2O {

    public H2O() {

    }

    Lock lock = new ReentrantLock();
    Condition c = lock.newCondition();//H的数量
    volatile int index = 0;

    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
        lock.lock();
        try {
            while (index == 2) {
                c.await();
            }
            // releaseHydrogen.run() outputs "H". Do not change or remove this line.
            releaseHydrogen.run();
            index++;
            c.signalAll();
        } finally {
            lock.unlock();
        }
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
        lock.lock();
        try {
            while (index < 2) {
                c.await();
            }
            releaseOxygen.run();
            index = 0;
            c.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

3.CyclicBarrier(这个我没看懂……)

class H2O {

    public H2O() {
        
    }

    ConcurrentLinkedQueue<Runnable> callbackOQ = new ConcurrentLinkedQueue<>();
    ConcurrentLinkedQueue<Runnable> callbackHQ = new ConcurrentLinkedQueue<>();
    CyclicBarrier cb = new CyclicBarrier(2,new Runnable() {
		
	@Override
	public void run() {
		callbackHQ.poll().run();
		callbackHQ.poll().run();
		while(callbackOQ.size()==0);
		callbackOQ.poll().run();
	}
    });
	
    public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
		callbackHQ.offer(releaseHydrogen);
	try {
		cb.await();
	} catch (BrokenBarrierException e) {
	}
    }

    public void oxygen(Runnable releaseOxygen) throws InterruptedException {
    	callbackOQ.offer(releaseOxygen);
    }
}

引用:
玩转Leetcode多线程——JAVA线程协助工具类实战
Semaphore的工作原理及实例
CountDownLatch的理解和使用
Java并发编程之CyclicBarrier详解








































































































以上是关于LeetCode——多线程问题汇总的主要内容,如果未能解决你的问题,请参考以下文章

多线程 Thread 线程同步 synchronized

多个用户访问同一段代码

Java多线程编程基础知识汇总

LeetCode(多线程)- 1242. 多线程网页爬虫

线程学习知识点总结

java 多线程40个问题汇总(转)