LeeCode多线程题目实战

391 阅读7分钟

1、按序打印

原地址:leetcode-cn.com/problems/pr…

1-1、题目

我们提供一个类,

public class Foo {
  public void first() { print("first"); }
  public void second() { print("second"); }
  public void third() { print("third"); }
}

【三个不同的线程】将会【共用】一个 Foo 实例。  

  • 线程 A 将会调用 first() 方法 ;
  • 线程 B 将会调用 second() 方法 ;
  • 线程 C 将会调用 third() 方法 ;

请设计修改程序,以确保 second() 方法在 first() 方法之后被执行,third() 方法在 second() 方法之后被执行。

1-2、题目分析

  • 【线程顺序】执行,一个线程等待另一个线程执行完成;
  • 与线程创建顺序无关;
  • 编码拦中给的代码,每个方法抛出【InterruptedException】异常,就会让人联想到Java中会抛出【InterruptedException】异常的方法,然后看看这些方法是不是可以解决;

1-3、结合Java提供的同步工具分析

1、Thread.sleep

最简单也最容易想到的就是【Thread.sleep】方法,这个方法虽然可以执行,但是【提交】后会【超出时间限制】,因此这个方法不被考虑;也不建议大家在平时工作中使用这个方法,因为Java提供了很多好的同步工具类;

2、synchronized + 条件变量

使用 synchronized + 条件判断变量,比如【first完成】和【second完成】,只有当【first】完成后,second才能执行,当second完成,third可以执行;

类中定义一个锁变量,当进入synchronized代码块时,必须先获取锁,此时做条件判断(注意,wait的条件判断必须是在循环中),当条件不满足时,调用【wait】方法,此时线程会释放锁,等待被唤醒后才能继续执行;

3、CountDownLatch

CountDownLatch设计目的,就是保证【一个线程】等待【另一个线程】执行完后继续处理,例如,给 first 的 CountDownLatch设置倒数为1,second等待 first的 CountDownLatch,当countDown后为0,second继续执行;

CountDownLatch的【计数】不能重置,到0就结束;把 CountDownLatch 的【countDown()】 和【awati()】方法配合使用,就可以完成【线程顺序执行】的功能; 

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

4、CyclicBarrier

CyclicBarrier也是一组线程相互等待的实现,只是与CountDownLatch不同,CyclicBarrier的【计数】可以通过【reset()】进行重置;

例如,设计两个CyclicBarrier,first打印完成,第一个CyclicBarrier等待,接着在second打印前也 加上 第一个CyclicBarrier 的等待;这里需要注意,CyclicBarrier的await方法除了会抛出 InterruptedException之外,还会抛出【BrokenBarrierException】异常,所以在代码中需要catch这个异常;

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.

5、Semaphore

Semaphore作为信号量的实现,在Java中对Semaphore有这样的说明,参考【Java内存模型】的【Happens-Before原则】中对于【管程锁定原则】的实现;所以只要在 first 方法中【release】信号量,在 second 方法中 【acquire】 同一个信号量,也能实现顺序执行;

Memory consistency effects: Actions in a thread prior to calling a "release" method such as release() happen-before actions following a successful "acquire" method such as acquire() in another thread.

关于【管程锁定原则】:

  • 在监视器锁上的【解锁】操作必须在【同一个监视器锁】的【加锁】操作之前执行;

6、无锁方案

无锁方法就是使用原子类进行判断,类似条件判断,但是没有锁,比如当原子类值AtomicInteger 不等于 1(类似 synchronized + 条件变量 判断),另一个方法不能执行,用这个方法完成线程顺序执行;

1-4、编码实现

1-4-1、CountDownLatch

class Foo {

    private CountDownLatch firstLatch;

    private CountDownLatch secondLatch;

    public Foo() {
        firstLatch = new CountDownLatch(1);
        secondLatch = new CountDownLatch(1);
    }

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run(); 
        // first执行完,firstLatch进行countDown,此时count=0,await的线程收到后进行执行
        firstLatch.countDown();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        // 等待firstLatch结束 
        firstLatch.await();
        printSecond.run(); 
        // second执行完,secondLatch进行countDown,此时count=0,await的线程收到后进行执行 
        secondLatch.countDown(); 
    }

    public void third(Runnable printThird) throws InterruptedException {
        // 等待secondLatch结束
        secondLatch.await(); 
        printThird.run(); 
    }
}

1-4-2、CyclicBarrier

class Foo {

    private CyclicBarrier firstBarrier;    

    private CyclicBarrier secondBarrier;

    public Foo() { 
        firstBarrier = new CyclicBarrier(2); 
        secondBarrier = new CyclicBarrier(2);
    }

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run(); 
        try {
            firstBarrier.await();        
        } catch (BrokenBarrierException e) {
        }
    }

    public void second(Runnable printSecond) throws InterruptedException {
        try {
            firstBarrier.await();
        } catch (BrokenBarrierException e) {
        } 
        printSecond.run();        
        try {
            secondBarrier.await(); 
        } catch (BrokenBarrierException e) {
        }    
    }

    public void third(Runnable printThird) throws InterruptedException {
        try {
            secondBarrier.await();
        } catch (BrokenBarrierException e) { 
        } 
        printThird.run();
    }
}

1-4-3、Semaphore

class Foo {

    private Semaphore secondSemaphore = new Semaphore(0);
    private Semaphore thirdSemaphore = new Semaphore(0);

    public Foo() {

    }

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run();
        // 释放信号量
        secondSemaphore.release();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        // 获取信号量,符合Happens-Before原则
        secondSemaphore.acquire();
        printSecond.run();
        thirdSemaphore.release();
    }

    public void third(Runnable printThird) throws InterruptedException {
        thirdSemaphore.acquire();
        printThird.run();
    }
}

1-4-4、synchronized + 条件变量

class Foo {

    private Object lock;

    private boolean firstFinished;

    private boolean secondFinished;    

    public Foo() {
        lock = new Object();
        firstFinished = false;
        secondFinished = false;
    }

    public void first(Runnable printFirst) throws InterruptedException {
        synchronized(lock) {
            printFirst.run(); 
            firstFinished = true;
            lock.notifyAll();
        }    
    }

    public void second(Runnable printSecond) throws InterruptedException {
        synchronized(lock) { 
            while (!firstFinished) {
                lock.wait();
            }
            printSecond.run();
            secondFinished = true;
            lock.notifyAll();
        }
    }

    public void third(Runnable printThird) throws InterruptedException {
        synchronized(lock) {
            while (!secondFinished) {
                lock.wait();
            }
            printThird.run();
            lock.notifyAll();
        }
    }
}

1-4-5、无锁变量

class Foo {

    private AtomicInteger firstDone;

    private AtomicInteger secondDone;

    public Foo() {
        firstDone = new AtomicInteger(0);
        secondDone = new AtomicInteger(0);
    }

    public void first(Runnable printFirst) throws InterruptedException {
        printFirst.run(); 
        firstDone.incrementAndGet();
    }

    public void second(Runnable printSecond) throws InterruptedException {
        while (firstDone.get() != 1) {
        }        
        printSecond.run();
        secondDone.incrementAndGet();
    }

    public void third(Runnable printThird) throws InterruptedException {
        while (secondDone.get() != 1) {
        }
        printThird.run();
    }
}

2、两个线程交替打印方法

原题地址:leetcode-cn.com/problems/pr…

2-1、题目

我们提供一个类,

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

2-2、解题思路

1、最简单的方法就是在方法中根据【计数n】,调用线程Thread的sleep方法,执行foo方法,sleep时间为 2n ,执行 bar方法 sleep时间为 2n + 1,类似 单双数 交替执行,但是这种方法容易导致超时,所以忽略;

2、回到题目,要求是【两个线程】,先后执行foo和bar方法,联想到【等待-通知】机制,给定一个条件值,当【条件满足】调用foo,当【条件不满足】调用bar,因为是在循环中调用,那么调用一次,条件值切换,最简单的使用一个 【boolean 值】 作条件判断;实现代码:

这里需要对【原题的代码】进行修改,将 【i++】循环控制变量,放到for中,否则第一次执行完,就会执行 循环控制语句,这样【当条件满足后】进入循环,boolean已经被修改,无法继续执行,如果要求不改动代码,这个实现不满足;这样就该就类似将【for循环】改成【while循环】;

public class FooBar { 
    private int n;

    private boolean fooPrint = true;

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

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

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; ) {
             if (!fooPrint) { 
                printBar.run();
                fooPrint = true;                // 不能在for中执行
                i++;
             } 
        }
    }
}

3、如果不能更新题目本身的代码,意味着【不能用boolean条件变量,作为方法执行】根据,那么只能通过其他手段实现类似【等待-通知】机制,Java提供的阻塞锁和同步器如下:

ReentrantLock,类似 synchronized的互斥锁,不能实现这个功能;

CountDownLatch,适用于类似【倒计时】的计数器执行完成,比如 foo 需要执行N次,等foo执行完成后,在处理后续操作,不适合用这种交替执行场景;虽然使用CountDownLatch可以实现,但是需要修改代码(假如要求不能修改代码,CountDownLatch 和 Lock都不太适合);

CyclicBarrier

Semaphore,作为信号量,完全可以实现【先后执行】的操作,执行foo方法时,【acquire】信号量fooSemaphore,当foo执行完成,【release】信号量barSemaphore;

2-3、Java代码实现

2-3-1、用Semaphore实现

class FooBar {
    private int n;

    private Semaphore fooSemaphore;

    private Semaphore barSemaphore;

    public FooBar(int n) {
        this.n = n;
        this.fooSemaphore = new Semaphore(1);
        this.barSemaphore = new Semaphore(0);
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            fooSemaphore.acquire();
            printFoo.run();
            barSemaphore.release();
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            barSemaphore.acquire();
            printBar.run();
            fooSemaphore.release();
        }
    }
}