多线程交替打印问题

字节三面中遇到的算法题是用多线程交替打印问题,总得来说是一道线程通信问题。这类问题有如下形式:

  • 三个线程T1、T2、T3轮流打印ABC,打印n次,如ABCABCABCABC…….
  • 两个线程交替打印1-100的奇偶数
  • N个线程循环打印1-100

本博文代码对应题目为:两个线程交替打印 1-10 的奇偶数。连带着复习一下线程通信的方式。

线程通信的方式

首先需要明确一个前置问题 —— 线程间通信的方式有哪些?

synchronized + wait()/notify()

  • 拿到锁后,看是不是轮到本线程?
    • 如果没有轮到本线程,调用 wait() 转为 wait 状态,并释放锁。此时其他线程可以拿到锁。
    • 如果轮到本线程,执行输出,结束后调用 notify(),让等待状态的线程判断是否轮到自己。
public class Solution1 {
    // synchronized 单锁方案
    private static int count = 1;
    private static final int THREAD_COUNT = 2;
    private static final Object LOCK = new Object();


    private static class TestRunnable implements Runnable {
        private int sign;
        public TestRunnable(int sign) {
            this.sign = sign;
        }
        @Override
        public void run() {
            synchronized (LOCK) {
                while (count <= 10) {
                    while (count % THREAD_COUNT != sign) {
                        // 1 还没有轮到这个线程的时候,用 wait() 释放锁,让该线程陷入 wait 状态,避免了空转
                        try {
                            LOCK.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }
                    // 2 轮到该线程
                    if (count <= 10) {
                        System.out.println(Thread.currentThread().getName() + ":" + count++);
                    }
                    // 3 唤醒其他 wait 状态的线程
                    LOCK.notifyAll();
                }
            }
        }
    }


    public static void main(String[] args) {
        new Thread(new TestRunnable(1), "Thread1").start();
        new Thread(new TestRunnable(0), "Thread2").start();
    }
}

Lock 接口

Lock 接口是 JUC 下的核心接口,其实现类 ReentrantLock 同样可以解决这类题目:

只使用一个 Lock 实际上和使用 synchronized 进行空转是一致的,这样解决的话,空转次数太多,会造成空转时资源浪费。

public class Solution_Lock {
    private static final Lock lock = new ReentrantLock();
    private static int count = 1;
    private static final int THREAD_COUNT = 2;

    private static class WaitThread extends Thread{

        private int sign;
        public WaitThread(int sign) {
            this.sign = sign;
        }

        @Override
        public void run() {
            while (count <= 10) {
                lock.lock();
                if (count <= 10 && count % THREAD_COUNT == sign) {
                    System.out.println(Thread.currentThread().getName() + ":" + count);
                    count++;
                }
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        new WaitThread(1).start();
        new WaitThread(0).start();
    }
}

Lock + Condition 通信

首先明确 ReentrantLockCondition 对应于 synchronizedwait()/notify() 机制,调用 condition.await() 同样会释放锁,让原线程陷入 wait 状态。

它们的不同是,synchronized 的通信机制是一个 synchronized 锁对应一组通信,而每一个 ReentrantLock 可以 new 出多个 Condition,实现更加精准的通信。下面是用 condition 机制实现的代码,基本原理和 synchronized 的实现思路一致:

public class Solution_Condition {
    private static final Lock LOCK = new ReentrantLock();
    private static final Condition C1 = LOCK.newCondition();
    private static final Condition C2 = LOCK.newCondition();
    private static final int MAX_COUNTER = 10;
    private static int count = 1;

    private static class TestRunnable implements Runnable {

        private int sign;
        private Condition currCondition;
        private Condition nextCondition;

        public TestRunnable(int sign, Condition currCondition, Condition nextCondition) {
            this.sign = sign;
            this.currCondition = currCondition;
            this.nextCondition = nextCondition;
        }

        @Override
        public void run() {
            LOCK.lock();
            try {
                while (count <= MAX_COUNTER) {
                    while (count % 2 != sign) currCondition.await();
                    if (count > MAX_COUNTER) break;
                    System.out.println(Thread.currentThread().getName() + ":" + count++);
                    nextCondition.signal();
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                LOCK.unlock();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TestRunnable(1, C1, C2), "thread1").start();
        new Thread(new TestRunnable(0, C2, C1), "thread2").start();
    }
}

JUC 工具类 —— Semaphore

Semaphore 是 Java 并发工具包 (java.util.concurrent) 中的一个基于计数的同步工具用于控制同时访问特定资源的线程数量。Semaphore内部维护了一个计数器,其值为可以访问的共享资源的 permit 个数。

基本概念

  • 它内部维护了一组许可(permit),可以看作是发放给线程的“许可证”。
  • 线程需要 permit 才能访问共享资源。
  • 如果没有可用的 permit,线程会阻塞直到有许可可用。

构造方法

允许用户指定初始许可数(permits)以及公平性策略(fair)。

public Semaphore(int permits) {}

public Semaphore(int permits, boolean fair) {}
  • @param permits  初始可用的许可数量。该值可以为负数,在这种情况下,必须先执行 release() 释放许可,  否则 acquire() 将不会被授予许可。
  • @param fair     如果 true,则信号量在竞争情况下保证先进先出(FIFO)顺序授予许可;  如果 false,则不保证公平性(默认行为)。

两个核心方法

  • semaphore.acquire() 获取许可,没有许可时线程会阻塞
  • semaphore.release() 释放许可,使其他线程可以继续执行。

值得注意的是,这两个函数都可以加入参 permits ,设置每次获取和释放需要的许可数。

总得来说,Semaphore 这个工具类提供了对共享资源/锁的许可量管理,允许对许可数量进行设置。

用这个工具类同样可以解决多线程交替打印问题,解决思路:

  • 初始化 Semaphore 变量,以奇数开始,所以 oddSemaphore 初始就有 1 个 permit。
  • 在奇数线程输出 1 后,给偶数线程对应的 evenSemaphore 释放 1 个 permit。
public class Solution_Semaphore {

    private static final Semaphore oddSemaphore = new Semaphore(1);
    private static final Semaphore evenSemaphore = new Semaphore(0);
    private static int count = 1;

    private static class TestRunnable implements Runnable {
        private Semaphore currSemaphore;
        private Semaphore nextSemaphore;
        public TestRunnable( Semaphore s1, Semaphore s2) {
            this.currSemaphore = s1;
            this.nextSemaphore = s2;
        }

        @Override
        public void run() {
            while (count <= 10) {
                try {
                    currSemaphore.acquire();
                    if (count > 10) break;
                    System.out.println(Thread.currentThread().getName() + ":" + count++ + " ");
                    nextSemaphore.release();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new TestRunnable(oddSemaphore, evenSemaphore), "thread-odd").start();
        new Thread(new TestRunnable(evenSemaphore, oddSemaphore), "thread-even").start();
    }
}

值得注意的是,除了 Semaphore ,JUC 还提供了另外两种常用的工具类,CountDownLatch 和 CyclicBarrier。但是这两个工具类不适合用在这种交替打印的场景,理由如下:

  • CountDownLatch 的设计初衷是用来等待一个或多个线程完成某个事件,一旦计数器降为零,就不能重置或再次使用。适合用于一次性的协调,比如等待多个线程初始化完成或等待任务全部结束。
  • Barriers 的设计初衷是让一组线程在某个同步点(屏障)相互等待,直到所有线程都到达这个点后,再一起继续执行。与 CountDownLatch 一次性协调的特点不同,Barriers(如 CyclicBarrier 和 Phaser)可以反复使用,适用于多阶段或迭代性的同步任务。

它们两个的场景是线程之间的协调,一个是递减的计数器(一次性),一个是递增的计数器(可重用),不适合于线程交替执行的场景。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇