面试题——并发情况下,顺序打印ABC

本文介绍了在并发环境下如何顺序打印ABC,通过Semaphore、Volatile和ReentrantLock三种方式实现。讨论了死锁的必要条件,并指出这些方法如何避免循环等待。示例代码展示了各种实现的思路和优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

太紧张了,好久没有走出面试的阴影,脑子一片空白,明明很多方案,却只写出来一种信号量方法,这里做一个简单的记录。

前置知识

死锁

线程组中的每一个线程都无法继续推进,都只能由其他线程进行唤醒。

死锁的必要条件

互斥、不剥夺、请求与保持、循环等待。这里顺序打印其实很好体现对循环等待的条件的破坏,也就是按照编号进行顺序上锁。

思路

并发的情况下,顺序打印,其实考察的就是对独占锁的使用。这里给出三种方法:信号量、Volatile、ReentrantLock。

Semaphore

面试的时候临场想出来的写法,只能说一点封装、面向对象的思想看不到,只能说相当丑陋。这里其实是一种变相的生产者消费者模式。

    @Test
    public void testForSemaphore() {
        Semaphore semaphoreA = new Semaphore (1);
        Semaphore semaphoreB = new Semaphore (0);
        Semaphore semaphoreC = new Semaphore (0);

        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    semaphoreA.acquire ();
                    printf ('A');
                } catch (InterruptedException e) {
                    throw new RuntimeException (e);
                } finally {
                    semaphoreB.release ();
                }
            }
        }, "t1").start ();
        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    semaphoreB.acquire ();
                    printf ('B');
                } catch (InterruptedException e) {
                    throw new RuntimeException (e);
                } finally {
                    semaphoreC.release ();
                }
            }
        }, "t2").start ();
        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                try {
                    semaphoreC.acquire ();
                    printf ('C');
                } catch (InterruptedException e) {
                    throw new RuntimeException (e);
                } finally {
                    semaphoreA.release ();
                }
            }
        }, "t3").start ();
    }

Volatile

通过Volatile对缓存的禁用,每次都去读取最新的token值,来决定放行哪一个行为。这其实有点CAS的味道。当然代码的简洁性稍微强了一丢丢,一点封装的味道都没有。

    volatile int token = 0;

    @Test
    public void testForVolatile() {
        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                while (token % 3 != 0) ;
                printf ('A');
                token++;
            }
        }, "t1").start ();
        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                while (token % 3 != 1) ;
                printf ('B');
                token++;
            }
        }, "t2").start ();
        new Thread (() -> {
            for (int i = 0; i < 20; i++) {
                while (token % 3 != 2) ;
                printf ('C');
                token++;
            }
        }, "t3").start ();
    }

这里附上使用Unsafe实现Volatile,其实就是手动进行缓存进行,禁止指令重排,每次都去内存中读取最新值,监听变量的变化

    @Test
    public void testForUnsafe() throws NoSuchFieldException, IllegalAccessException {
        Field field = Unsafe.class.getDeclaredField ("theUnsafe");
        field.setAccessible (true);
        Unsafe unsafe = (Unsafe) field.get (null);
        ExecutorService threadPool = Executors.newFixedThreadPool (3);
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            threadPool.execute (() -> {
                for (int j = 0; j < 20; j++) {
                    unsafe.loadFence ();
                    while (num % 3 != finalI);
                    char c = (char) ('A' + num % 3);
                    printf (c);
                    num++;
                }
            });
        }
    }

ReentrantLock

通过ReentrantLock,并对打印任务进行封装得以复用。每个线程都需要去抢占Lock,之后通过num的来决定打印的是什么。代码看起来舒服了很多,但是究竟哪个才是打印A的线程?哪个才是B?哪个才是C呢?其实职责就不明确了,这里是面向结果编程,丢失了具体类的职责,所以也不是很完美。

    private int num = 0;

    class PrintTask implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 20; i++) {
                try {
                    lock.lock ();
                    char c = (char) ('A' + num % 3);
                    num++;
                    printf (c);
                } finally {
                    lock.unlock ();
                }
            }
        }
    }

    @Test
    public void testForLock() {
        ExecutorService threadPool = Executors.newFixedThreadPool (3);
        for (int i = 0; i < 3; i++) {
            threadPool.execute (new PrintTask ());
        }
    }

总结

只面过两次厂,看到大佬们实属紧张,希望以后能有所改善!真的很难受,这么简单的题,却写出了最令人难以接受的代码,当然还有很多方式,只是不再写了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值