遇到个面试题,挺有意思

程序员鱼皮

共 28377字,需浏览 57分钟

 · 2022-07-11

大家好,我是鱼皮,最近看到一个面试题目,感觉挺有意思的,大意如下:

ok,大家看到这个题,可以先理解下,这里启动了两个线程,a 和 b,但是虽然说 a 在 b 之前 start,不一定就可以保证线程 a 的逻辑,可以先于线程 b 执行。

所以,这里的意思是,线程 a 和 b,执行顺序互不干扰,我们不应该假定其中一个线程可以先于另外一个执行。

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

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

可见性保证

程序里定义了一个全局变量,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,其他的阻塞队列也是可以的。

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();
    }
}

暂时想了这么些,方案还是比较多的,大家可以开动脑筋,头脑风暴吧。

看看你还有什么骚操作,可以在评论区留言。



以上就是本期分享了。

最后,欢迎加入 鱼皮的编程知识星球(点击了解详情),和大家一起交流学习编程,向鱼皮和大厂同学 1 对 1 提问、帮你制定学习计划不迷茫、跟着鱼皮直播做项目(往期项目可无限回看)、领取鱼皮原创编程学习/求职资料等。最近秋招开始了,星球内也会帮大家规划求职进度、完善简历和项目。


往期推荐

给大家鼓鼓劲!

我是怎么自学 Git / GitHub 的?

Websocket 可以玩出些什么花儿?

Web3.0开发快速入门

组里新入职一位【31岁】的校招生。。。

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报