多线程交替输出A1B2C3D4...你怎么实现?

简介: 多线程交替输出A1B2C3D4...你怎么实现?

引言

不知道最近有没有小伙伴去面试,今天了不起回想到了早期去面试遇到的一个多线程面试问题。

面试问题是一个笔试题:

两个线程依次交替输出A~Z,1到26,形如A1B2C3D4...

当时的我还很菜,用了原生的线程,借助wait和notify方法实现。

伙伴们你们也可以先暂停,自己思考下用什么方式来实现。

今天了不起和伙伴们一起来基于JDK1.8进行实现方式的探索,请看下文。

1. 使用线程方法

wait()方法会使当前线程释放锁,并进入等待状态,直到以下情况之一发生:

  1. 被其他线程调用notify()方法唤醒;
  2. 被其他线程调用notifyAll()方法唤醒;
  3. 被其他线程中断。

notify()方法用于唤醒一个正在等待的线程,使其从wait()方法中返回。

结合一个出让等待的机制,就这样交替实现。

public class T06_00_sync_wait_notify {
    public static void main(String[] args) {
        final Object o = new Object();
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        new Thread(()->{
            synchronized (o) {
                for(char c : aI) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait(); //让出锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify(); //必须,否则无法停止程序
            }
        }, "t1").start();
        new Thread(()->{
            synchronized (o) {
                for(char c : aC) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t2").start();
    }
}

运行结果:

思考:伙伴们,如果我想保证t2在t1之前打印,也就是说保证首先输出的是A而不是1,这个时候该如何做?

2. 使用CountDownLatch铁门闩

CountDownLatch是Java多线程中的一个同步工具类,它可以让一个或多个线程等待其他线程完成操作后再继续执行。

具体来说,CountDownLatch有两个主要方法:

  1. await()方法:调用该方法的线程会进入等待状态,直到计数器的值为0或者被中断;
  2. countDown()方法:调用该方法会将计数器减1,当计数器的值为0时,会唤醒所有等待的线程。
public class T07_00_sync_wait_notify {
    private static CountDownLatch latch = new CountDownLatch(1);
    public static void main(String[] args) {
        final Object o = new Object();
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        new Thread(()->{
            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (o) {
                for(char c : aI) {
                    System.out.print(c);
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t1").start();
        new Thread(()->{
            synchronized (o) {
                for(char c : aC) {
                    System.out.print(c);
                    latch.countDown();
                    try {
                        o.notify();
                        o.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                o.notify();
            }
        }, "t2").start();
    }
}

运行结果:

3. 使用ReentrantLock

我们可以通过ReentrantLock获取条件锁,通过它提供的方法来实现。

具体来说,ReentrantLock的Condition接口提供了以下三个方法:

  1. await()方法:当前线程进入等待状态,并释放锁,直到其他线程使用signal()或signalAll()方法唤醒它;
  2. signal()方法:唤醒一个等待在该条件上的线程;
  3. signalAll()方法:唤醒所有等待在该条件上的线程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T08_00_lock_condition {
    public static void main(String[] args) {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        Lock lock = new ReentrantLock();
        Condition condition = lock.newCondition();
        new Thread(()->{
            try {
                lock.lock();
                for(char c : aI) {
                    System.out.print(c);
                    condition.signal();
                    condition.await();
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        new Thread(()->{
            try {
                lock.lock();
                for(char c : aC) {
                    System.out.print(c);
                    condition.signal();
                    condition.await();
                }
                condition.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

运行结果:

Condition本质是锁资源上不同的等待队列,我们也可以获取不同的等待队列来实现。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class T09_00_lock_condition {
    public static void main(String[] args) {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        Lock lock = new ReentrantLock();
        Condition conditionT1 = lock.newCondition();
        Condition conditionT2 = lock.newCondition();
        new Thread(()->{
            try {
                lock.lock();
                for(char c : aI) {
                    System.out.print(c);
                    conditionT2.signal();
                    conditionT1.await();
                }
                conditionT2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t1").start();
        new Thread(()->{
            try {
                lock.lock();
                for(char c : aC) {
                    System.out.print(c);
                    conditionT1.signal();
                    conditionT2.await();
                }
                conditionT1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "t2").start();
    }
}

4. 使用TransferQueue阻塞队列

TransferQueue是Java并发包中的一个阻塞队列,它可以用于多线程之间的数据交换和同步。

LinkedTransferQueue继承自TransferQueue,并且还可以支持异步操作。

LinkedTransferQueue的take()方法和transfer()方法都是用于从队列中取出元素的方法,但它们的使用场景和行为有所不同。

take()方法是一个阻塞方法,它会一直阻塞直到队列中有可用元素,才将队列中的元素取出并返回。

transfer()方法也是一个阻塞方法,它会将指定的元素插入到队列中,并等待另一个线程从队列中取出该元素。如果队列中没有等待的线程,则当前线程会一直阻塞,直到有其他线程从队列中取走该元素为止。

那么我们就利用这一点它必须要另外一个线程来取进而实现把值交替输出。

import java.util.concurrent.LinkedTransferQueue;
import java.util.concurrent.TransferQueue;
public class T13_TransferQueue {
    public static void main(String[] args) {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        TransferQueue<Character> queue = new LinkedTransferQueue<Character>();
        new Thread(()->{
            try {
                for (char c : aI) {
                    System.out.print(queue.take());
                    queue.transfer(c);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1").start();
        new Thread(()->{
            try {
                for (char c : aC) {
                    queue.transfer(c);
                    System.out.print(queue.take());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2").start();
    }
}

运行结果:

5. 使用LockSupport

LockSupport是Java并发包中的一个工具类,它可以用于线程的阻塞和唤醒。

你可以把它类比成Object的wait()和notify()方法,但LockSupport是比它们更加灵活和可控的。

LockSupport提供了park()和unpark()方法:

当一个线程调用park()方法时,它会被阻塞,直到另一个线程调用该线程的unpark()方法才会被唤醒。

如果调用unpark()方法时,该线程还没有调用park()方法,则该线程调用park()方法时不会被阻塞,可以直接返回。

import java.util.concurrent.locks.LockSupport;
//Locksupport park 当前线程阻塞(停止)
//unpark(Thread t)
public class T02_00_LockSupport {
    static Thread t1 = null, t2 = null;
    public static void main(String[] args) throws Exception {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        t1 = new Thread(() -> {
                for(char c : aI) {
                    System.out.print(c);
                    LockSupport.unpark(t2); //叫醒T2
                    LockSupport.park(); //T1阻塞
                }
        }, "t1");
        t2 = new Thread(() -> {
            for(char c : aC) {
                LockSupport.park(); //t2阻塞
                System.out.print(c);
                LockSupport.unpark(t1); //叫醒t1
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}

运行结果:

6. 使用枚举类作同步标志

创建一个枚举类ReadyToRun,利用while(true)死等和枚举类指向对象不同作标志位交替输出。

public class T03_00_cas {
    enum ReadyToRun {T1, T2}
    static volatile ReadyToRun r = ReadyToRun.T1; 
    public static void main(String[] args) {
        char[] aI = "1234567".toCharArray();
        char[] aC = "ABCDEFG".toCharArray();
        new Thread(() -> {
            for (char c : aI) {
                while (r != ReadyToRun.T1) {}
                System.out.print(c);
                r = ReadyToRun.T2;
            }
        }, "t1").start();
        new Thread(() -> {
            for (char c : aC) {
                while (r != ReadyToRun.T2) {}
                System.out.print(c);
                r = ReadyToRun.T1;
            }
        }, "t2").start();
    }
}

运行结果:

总结

好了,关于这个面试题的解法了不起暂时就想到这6种情况。

这个面试题也是一道经典的多线程面试题,如果你能将这几种情况掌握,定会另面试官刮目相看。

如果你们还有新的方法欢迎和了不起一起探讨研究,毕竟代码是死的人是活的。

相关文章
|
NoSQL 网络协议 数据库
为什么 Lettuce 会带来更长的故障时间
本文详述了阿里云数据库 Tair/Redis 将使用长连接客户端在非预期故障宕机切换场景下的恢复时间从最初的 900s 降到 120s 再到 30s的优化过程,涉及产品优化,开源产品问题修复等诸多方面。
68998 11
为什么 Lettuce 会带来更长的故障时间
|
SQL 存储 监控
深入可观测底层:OpenTelemetry 链路传递核心原理
本文会系统讲解链路传递一些基本概念,同时结合案例讲解链路传递的过程。
3380 1
深入可观测底层:OpenTelemetry 链路传递核心原理
|
弹性计算 网络协议 数据处理
稳定平滑进行云上业务IPv6化改造—— Series1:改造思路及CDN改造
随着国家工信部印发的《推进IPv6规模部署行动计划》的深入推进,近期国资委相关的大型国企都开始着手进行业务的IPv6化改造,其在阿里云上的门户及B2B、B2C等对外业务,自然进入第一批改造的范围。本文是基于在具体客户的IPv6化过程中积累的最佳实践编写,希望能够给读者带来一些IPv6化改造的启发。
稳定平滑进行云上业务IPv6化改造—— Series1:改造思路及CDN改造
|
Java Maven
最快的 maven repository--阿里镜像仓库
国内速度超快的maven repository
161451 0
|
Java Spring
Springboot-starter的自动配置原理-及案例实现
Springboot-starter的自动配置原理-及案例实现
300 72
|
运维 Kubernetes Devops
阿里云云效操作报错合集之k8s直接返回401,该如何排查
本合集将整理呈现用户在使用过程中遇到的报错及其对应的解决办法,包括但不限于账户权限设置错误、项目配置不正确、代码提交冲突、构建任务执行失败、测试环境异常、需求流转阻塞等问题。阿里云云效是一站式企业级研发协同和DevOps平台,为企业提供从需求规划、开发、测试、发布到运维、运营的全流程端到端服务和工具支撑,致力于提升企业的研发效能和创新能力。
阿里云云效操作报错合集之k8s直接返回401,该如何排查
|
存储 消息中间件 JSON
DDD基础教程:一文带你读懂DDD分层架构
DDD基础教程:一文带你读懂DDD分层架构
|
消息中间件 安全 Java
一文读懂RabbitMQ核心概念及架构
一文读懂RabbitMQ核心概念及架构
|
缓存 Java Maven
Maven基础篇:拉取依赖没有任何报错死活拉不下来
Maven基础篇:拉取依赖没有任何报错死活拉不下来
|
JavaScript Java Apache
【SpringBoot】Maven 版本管理与 flatten-maven-plugin 插件的使用及分析
【SpringBoot】Maven 版本管理与 flatten-maven-plugin 插件的使用及分析
5112 0