用LockSupport解Leetcode1116并发题,请教下哪里出了问题

zzw00001 2020-06-12 01:43:49
原题链接:https://leetcode-cn.com/problems/print-zero-even-odd/

问题
死磕了2天了,不弄明白切鸡儿。大概是在几百次运行后会有明显的同步问题导致结果错误,有时也会莫名其妙的阻塞住。不知道老哥们有何高见,换解法就不必了~

我的解法
main方法测试用了输出重定向,然后获取打印值进行判断,和期望值不符则break。同时还依赖一个把类似Consumer函数封装成Runnable并try catch异常的接口,嫌麻烦可以自己写测试。

public class Solution1116 {

static CountDownLatch cdl = new CountDownLatch(3);

public static void main(String[] args) {
ZeroEvenOdd zeo = new ZeroEvenOdd(9);

// 重定向输出流相关操作
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
PrintStream old = System.out;
System.setOut(ps);

// 初始化线程和3个Runnable
IntConsumer ic = System.out::print;
ExecutorService es = Executors.newFixedThreadPool(3);
Runnable r0 = ConsumerCatch.runnableWrap(zeo::zero, ic); // 需要ConsumerCatch接口的支持
Runnable r1 = ConsumerCatch.runnableWrap(zeo::odd, ic);
Runnable r2 = ConsumerCatch.runnableWrap(zeo::even, ic);
while (true) {
es.submit(r0);
es.submit(r1);
es.submit(r2);
try {
cdl.await(); // 阻塞主线程
} catch (InterruptedException e) {
e.printStackTrace();
}

// 打印并比对结果
ps.flush();
System.setOut(old);
String s = baos.toString();
System.out.println(s);
if (!s.equals("010203040506070809")) {
break;
}

// 全体重置
baos.reset();
cdl = new CountDownLatch(3);
zeo.reset();
System.setOut(ps);
}
}

private static class ZeroEvenOdd {

private int n;
private volatile int current = 1; // 弃用
private AtomicInteger ai = new AtomicInteger(1);

private volatile Thread t0 = null;
private volatile Thread t1 = null;
private volatile Thread t2 = null;

public ZeroEvenOdd(int n) {
this.n = n;
}

// 测试用,用来使该对象恢复到初始状态
public void reset() {
ai.set(1);
t0 = null;
t1 = null;
t2 = null;
}

// printNumber.accept(x) outputs "x", where x is an integer.
public void zero(IntConsumer printNumber) throws InterruptedException {
t0 = Thread.currentThread();
for (int i = 0; i < n; i++) {
printNumber.accept(0);
while (t1 == null || t2 == null) {} // 自旋保证初始化 + 内存屏障
if ((ai.get() & 1) == 1)
LockSupport.unpark(t1);
else
LockSupport.unpark(t2);
if (i != n - 1)
LockSupport.park();
}
Solution1116.cdl.countDown(); // 仅测试,用来给主线程放行
}

public void even(IntConsumer printNumber) throws InterruptedException {
t2 = Thread.currentThread();
for (int i = 0; i < n / 2; i++) {
LockSupport.park();
printNumber.accept(ai.getAndIncrement());
LockSupport.unpark(t0);
}
Solution1116.cdl.countDown(); // 仅测试,用来给主线程放行
}

public void odd(IntConsumer printNumber) throws InterruptedException {
t1 = Thread.currentThread();
for (int i = 0; i < n - n / 2; i++) {
LockSupport.park();
printNumber.accept(ai.getAndIncrement());
LockSupport.unpark(t0);
}
Solution1116.cdl.countDown(); // 仅测试,用来给主线程放行
}
}
}


main方法所依赖的接口
主要是为了少写try catch,就整了一个
@FunctionalInterface
public interface ConsumerCatch<T>{

static <T> Runnable runnableWrap(ConsumerCatch<T> ct, T t) {
return () -> {
try {
ct.accept(t);
} catch (Exception e) {
e.printStackTrace();
}
};
}

static <T> Thread threadWrap(ConsumerCatch<T> ct, T t) {
return new Thread(() -> {
try {
ct.accept(t);
} catch (Exception e) {
e.printStackTrace();
}
});
}

void accept(T t) throws Exception;
}
...全文
139 2 打赏 收藏 转发到动态 举报
写回复
用AI写文章
2 条回复
切换为时间正序
请发表友善的回复…
发表回复
zzw00001 2020-06-12
  • 打赏
  • 举报
回复
好了,我知道问题在哪了。

if (i != n - 1)
LockSupport.park();

问题就处在zero方法的这个if判断上。它导致每次运行park和unpark的次数不同,从而每次都会给一个线程多发放一次permit,也就是下一次park的豁免权。去掉就没有问题了。

至于为什么使用fixed线程池,这个错误会最多扛到5000次运行才出现,而每次都新创建线程这个错误只要2次就出现,目前我还无法解释。需要了解这个permit到底是跟谁走的,如果跟线程走,那么每次新建线程肯定不享有之前的豁免权,这就很反直觉。
zzw00001 2020-06-12
  • 打赏
  • 举报
回复
像这种运行N次才出现问题的多线程程序,你们会怎么debug呢。我表示很缺这个方法论,知道了这个问题我也能自己再死磕下去了。

62,614

社区成员

发帖
与我相关
我的任务
社区描述
Java 2 Standard Edition
社区管理员
  • Java SE
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧