zoukankan      html  css  js  c++  java
  • 曹工说面试题:一个线程协同问题,解法繁多,都要被玩坏了,趁着没坏,一起玩吧

    前言

    最近两个月写文章很少,因为自己学习状态也不是很好,我看了下,上一篇文章,都是一个月前了。

    不知道大家有没有感觉,小学初中读的一些书,看的一些文章,到现在都印象深刻,反倒是高中学的知识,高考后就慢慢消散,直到遗忘。

    我想说的是,记得初中学过鲁迅的《藤野先生》,里面有一段话,大意是:久了不联系,有时候想联系,却又无从下笔,到最后就更是不了了之了。

    我找了下原文:

    将走的前几天,他叫我到他家里去,交给我一张照相,后面写着两个字道:“惜别”,还说希望将我的也送他。但我这时适值没有照相了;他便叮嘱我将来照了寄给他,并且时时通信告诉他此后的状况。

    我离开仙台之后,就多年没有照过相,又因为状况也无聊,说起来无非使他失望,便连信也怕敢写了。经过的年月一多,话更无从说起,所以虽然有时想写信,却又难以下笔,这样的一直到现在,竟没有寄过一封信和一张照片。从他那一面看起来,是一去之后,杳无消息了。

    其实写文章也是这样的,久了不写更不想写,但是心里又时时记着这么个事情,玩也不是很自在;今天先随便写一下,找下状态吧,因为现在文章可能在博客和公众号发,比如博客,一般来说会随意点,但是公众号的话,一般大家质量要求会高一些,结果就是,为了追求高质量,而非要找到一些很厉害的技术点,或者自己研究透了才动笔,这样会导致一些想法难产,因为可能觉得很简单,不值得发到公众号,实际上,很多时候都是浮于表面地觉得很简单,一旦深挖,立马就废。

    扯这么多,也是给我自己,或者其他刚开始写技术公众号的同学,也不用觉得非要写的多么多么好才发出来,本来大家都是一步一步来的,各种大佬也不是一下就变成大佬的,把自己的学习过程和成长过程发出来,大家也就知道:哦,大佬原来也这么菜啊,哈哈。

    比如最近看到一些算法大佬,一开始也是10道算法题,全部都要看答案的好么。。

    扯了不少,言归正传吧,最近在网上看到一个面试题目,感觉挺有意思的,大意如下:

    ok,大家看到这个题,可以先理解下,这里启动了两个线程,a和b,但是虽然说a在b之前start,不一定就可以保证线程a的逻辑,可以先于线程b执行,所以,这里的意思是,线程a和b,执行顺序互不干扰,我们不应该假定其中一个线程可以先于另外一个执行。

    另外,既然是面试题,那常规做法自然是不用上了,比如让b先sleep几秒钟之类的,如果真这么答,那可能面试就结束了吧。

    ok,我们下面开始分析解法。

    可见性保证

    程序里定义了一个全局变量,var = 1;线程a会修改这个变量为2,线程b则在变量为2时,执行自己的业务逻辑。

    那么,这里首先,我们要做的是,先讲var使用volatile修饰,保证多线程操作时的可见性。

    public static volatile int var = 1;
    

    解法分析

    经过前面的可见性保证的分析,我们知道,要想达到目的,其实就是要保证:

    a中的对var+1的操作,需要先于b执行。

    但是,现在的问题是,两个线程同时启动,不知道谁先谁后,怎么保证a先执行,b后执行呢?

    让线程b先不执行,大概有两种思路,一种是阻塞该线程,一种是不阻塞该线程,阻塞的话,我们可以想想,怎么阻塞一个线程。

    大概有:

    • synchronized,取不到锁时,阻塞
    • java.util.concurrent.locks.ReentrantLock#lock,取不到锁时,阻塞
    • object.wait,取到synchronized了,但是因为一些条件不满足,执行不下去,调用wait,将释放锁,并进入等待队列,线程暂停运行
    • java.util.concurrent.locks.Condition.await,和object.wait类似,只不过object.wait在jvm层面,使用c++实现,Condition.await在jdk层面使用java语言实现
    • threadA.join(),等待对应的线程threadA执行完成后,本线程再继续运行;threadA没结束,则当前线程阻塞;
    • CountDownLatch#await,在对应的state不为0时,阻塞
    • Semaphore#acquire(),在state为0时(即剩余令牌为0时),阻塞
    • 其他阻塞队列、FutureTask等等

    如果不让线程进入阻塞,则一般可以让线程进入一个while循环,循环的退出条件,可以由线程a来修改,线程a修改后,线程b跳出循环。

    比如:

    volatile boolean stop = false;
    while (!stop){
        ...
    }
    

    上面也说了这么多了,我们实际上手写一写吧。

    错误解法1--基于wait

    下面的思路是基于wait、notify;线程b直接wait,线程a在修改了变量后,进行notify。

    public class Global1 {
        public static volatile int var = 1;
        public static final Object monitor = new Object();
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                // 1
                Global1.var++;
                // 2
                synchronized (monitor) {
                    monitor.notify();
                }
            });
            Thread b = new Thread(() -> {
                // 3
                synchronized (monitor) {
                    try {
                        monitor.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 4
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            a.start();
            b.start();
        }
    }
    

    大家觉得这个代码能行吗?实际是不行的。因为实际的顺序可能是:

    线程a--1
    
    线程a--2
    
    线程b--1
    
    线程b--2
    
    

    在线程a-2时,线程a去notify,但是此时线程b还没开始wait,所以此时的notify是没有任何效果的:没人在等,notify个锤子。

    怎么修改,本方案才行得通呢?

    那就是,修改线程a的代码,不要急着notify,先等等。

            Thread a = new Thread(() -> {
                Global1.var++;
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (monitor) {
                    monitor.notify();
                }
            });
    

    但是这样的话,明显不合适,有作弊嫌疑,也不优雅。

    错误解法2--基于condition的signal

    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.ReentrantLock;
    
    public class Global1 {
        public static volatile int var = 1;
        public static final ReentrantLock reentrantLock = new ReentrantLock();
        public static final Condition condition = reentrantLock.newCondition();
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                final ReentrantLock lock = reentrantLock;
                lock.lock();
                try {
                    condition.signal();
                } finally {
                    lock.unlock();
                }
            });
            Thread b = new Thread(() -> {
                final ReentrantLock lock = reentrantLock;
                lock.lock();
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            a.start();
            b.start();
        }
    }
    
    

    这个方案使用了Condition对象来实现object的notify、wait效果。当然,这个也有同样的问题。

    正确解法1--基于错误解法2进行改进

    我们看看,前面问题的根源在于,我们线程a,在去通知线程b的时候,有可能线程b还没开始wait,所以此时通知失效。

    那么,我们是不是可以先等等,等线程b开始wait了,再去通知呢?

            Thread a = new Thread(() -> {
                Global1.var++;
                final ReentrantLock lock = reentrantLock;
                lock.lock();
                try {
                    // 1
                    while (!reentrantLock.hasWaiters(condition)) {
                        Thread.yield();
                    }
                    condition.signal();
                } finally {
                    lock.unlock();
                }
            });
    

    1处代码,就是这个思想,在signal之前,判断当前condition上是否有waiter线程,如果没有,就死循环;如果有,才去执行signal。

    这个方法实测是可行的。

    正确解法2

    对正确解法1,换一个api,就变成了正确解法2.

    Thread a = new Thread(() -> {
        Global1.var++;
        final ReentrantLock lock = reentrantLock;
        lock.lock();
        try {
            // 1
            while (reentrantLock.getWaitQueueLength(condition) == 0) {
                Thread.yield();
            }
            condition.signal();
        } finally {
            lock.unlock();
        }
    });
    

    1这里,获取condition上等待队列的长度,如果为0,说明没有等待者,则死循环。

    正确解法3--基于Semaphore

    刚开始,我们初始化一个信号量,state为0. 线程b去获取信号量的时候,就会阻塞。

    然后我们线程a再去释放一个信号量,此时线程b就可以继续执行。

    public class Global1 {
        public static volatile int var = 1;
        public static final Semaphore semaphore = new Semaphore(0);
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                semaphore.release();
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    semaphore.acquire();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    正确解法4--基于CountDownLatch

    public class Global1 {
        public static volatile int var = 1;
        public static final CountDownLatch countDownLatch = new CountDownLatch(1);
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                countDownLatch.countDown();
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    正确解法5--基于BlockingQueue

    这里使用了ArrayBlockingQueue,其他的阻塞队列也是可以的。

    import countdown.CountdownTest;
    
    
    public class Global1 {
        public static volatile int var = 1;
        public static final ArrayBlockingQueue arrayBlockingQueue = new ArrayBlockingQueue<Object>(1);
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                arrayBlockingQueue.offer(new Object());
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    arrayBlockingQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    
    

    正确解法6--基于FutureTask

    我们也可以让线程b等待一个task的执行结果;而线程a在执行完修改var为2后,执行该任务,任务执行完成后,线程b就会被通知继续执行。

    public class Global1 {
        public static volatile int var = 1;
        public static final FutureTask futureTask = new FutureTask<Object>(new Callable<Object>() {
            @Override
            public Object call() throws Exception {
                System.out.println("callable task ");
                return null;
            }
        });
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                futureTask.run();
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    futureTask.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    正确解法7--基于join

    这个可能是最简洁直观的,哈哈。也是群里同学们提供的解法,真的有才!

    public class Global1 {
        public static volatile int var = 1;
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    a.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    正确解法8--基于CompletableFuture

    这个和第6种类似。都是基于future。

    public class Global1 {
        public static volatile int var = 1;
        public static final CompletableFuture<Object> completableFuture =
                new CompletableFuture<Object>();
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                completableFuture.complete(new Object());
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                try {
                    completableFuture.get();
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    非阻塞--正确解法9--忙等待

    这种代码量也少,只要线程b在变量为1时,死循环就行了。

    public class Global1 {
        public static volatile int var = 1;
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                while (var == 1) {
                    Thread.yield();
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    非阻塞--正确解法10--忙等待

    忙等待的方案很多,反正就是某个条件不满足时,不阻塞自己,阻塞了会释放cpu,我们就是不希望释放cpu的。

    比如像下面这样也可以。

    public class Global1 {
        public static volatile int var = 1;
        public static final AtomicInteger atomicInteger =
                new AtomicInteger(1);
    
        public static void main(String[] args) {
            Thread a = new Thread(() -> {
                Global1.var++;
                atomicInteger.set(2);
            });
            a.setName("thread a");
            Thread b = new Thread(() -> {
                while (true) {
                    boolean success = atomicInteger.compareAndSet(2, 1);
                    if (success) {
                        break;
                    } else {
                        Thread.yield();
                    }
                }
    
                if (Global1.var == 2) {
                    //do something;
                    System.out.println(Thread.currentThread().getName() + " good job");
                }
            });
            b.setName("thread b");
            a.start();
            b.start();
        }
    }
    

    小结

    暂时想了这么写,方案还是比较多的,大家可以开动脑筋,头脑风暴吧!我是逐日,混迹成都的老java程序猿,博客里有我更多的一些文章,大家可以看看,暂时没有迁移到公众号的打算。

  • 相关阅读:
    细数阿里云在使用 Docker 过程中踩过的那些坑
    细数阿里云在使用 Docker 过程中踩过的那些坑
    javascript – 从页面停用浏览器打印选项(页眉,页脚,页边距)?
    jquery children()方法
    jquery 获取输入框的值
    java BigDecimal加减乘除
    window.print()打印时,如何自定义页眉/页脚、页边距
    深入分析:12C ASM Normal冗余中PDB文件块号与AU关系与恢复
    我不是药神,救不了你的穷根
    Install fail! Error: EPERM: operation not permitted
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/13737109.html
Copyright © 2011-2022 走看看