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

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

  • 前言
  • 前置知识
    • 死锁
    • 死锁的必要条件
  • 思路
    • Semaphore
    • Volatile
    • ReentrantLock
  • 总结

前言

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

前置知识

死锁

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

死锁的必要条件

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

思路

并发的情况下,顺序打印,其实考察的就是对独占锁的使用。这里给出三种方法:信号量、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 ());
        }
    }

总结

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

你可能感兴趣的:(java,java,开发语言,并发,面试题)