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程序猿,博客里有我更多的一些文章,大家可以看看,暂时没有迁移到公众号的打算。

  • 相关阅读:
    JavaScript
    94.Binary Tree Inorder Traversal
    144.Binary Tree Preorder Traversal
    106.Construct Binary Tree from Inorder and Postorder Traversal
    105.Construct Binary Tree from Preorder and Inorder Traversal
    90.Subsets II
    78.Subsets
    83.Merge Sorted Array
    80.Remove Duplicates from Sorted Array II
    79.Word Search
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/13737109.html
Copyright © 2011-2022 走看看