LeetCode|交替打印FooBar
题目连接,如下,输入一个n,交替打印Foo和Bar,各n次。
class FooBar {
public void foo() {
for (int i = 0; i < n; i++) {
print("foo");
}
}
public void bar() {
for (int i = 0; i < n; i++) {
print("bar");
}
}
}
看到这道题的第一反应,要么是锁了,要么是自旋了。首先,先试试自旋,如果自旋可以,就不用锁了。很快代码如下,很显然运行后就立即报错了Thrown exception java.lang.IllegalMonitorStateException,这个错误也算很常见了,wait方法只能由占有锁(严格来说是当前线程拥有对象的监视器)的线程主动调用才能生效:
private volatile boolean flag = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (flag) {
wait();
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
flag = true;
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (!flag) {
wait();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
flag = false;
}
}
于是加了判断,只有持有当前对象的监视器的线程才能进行wait,于是采用holdsLock
方法进行判断,这下子测试用例可以过了,但在n=5的时候,就超时了。:
private volatile boolean flag = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (flag) {
if (Thread.holdsLock(this)) {
wait();
}
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
flag = true;
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (!flag) {
if (Thread.holdsLock(this)) {
wait();
}
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
flag = false;
}
}
既然如此,我干脆不用wait了,直接自旋,毫无疑问又是超时了。仔细想想超时原因,其实,自旋的目的不就是为了让不符合条件的打印先缓一缓吗但问题是,不符合条件的打印进入自旋了,**符合条件的线程就会马上执行吗?**我们知道,多个线程的调度是随机的,谁能抢到CPU的控制权就执行谁,那么就可能存在一种情况,进入自旋的线程总能先一步的抢到CPU的控制权,然后总是执行,才会造成时间上的不断消费。
private volatile boolean flag = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (flag) {
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
flag = true;
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (!flag) {
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
flag = false;
}
}
知道原因,是否有一种方法可以在不符合条件时,让该线程让出CPU的控制权吗?说到这里很多人大概能猜到了。那就是Thread.yield()
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private volatile boolean flag = false;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (flag) {
Thread.yield();
}
// printFoo.run() outputs "foo". Do not change or remove this line.
printFoo.run();
flag = true;
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
while (!flag) {
Thread.yield();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
flag = false;
}
}
}
耗时和所占空间如下:
执行用时:25 ms
内存消耗:39.4 MB
那通过锁的方式呢?锁的话又重入锁、同步锁。这里试着用重入锁进行了实现,原理差不多,利用Condition创建了两个条件队列。
每次打印后,更改flag 标识,并唤醒对方法条件队列中的线程,当不符合条件的线程获得锁时,根据flag,会调用await进入等待队列 :
class FooBar {
private int n;
public FooBar(int n) {
this.n = n;
}
private static Lock lock = new ReentrantLock();
private static Condition foo = lock.newCondition();
private static Condition bar = lock.newCondition();
private static boolean flag = true;
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
// printFoo.run() outputs "foo". Do not change or remove this line.
lock.lock();
if (!flag) {
foo.await();
}
printFoo.run();
flag = false;
bar.signal();
lock.unlock();
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
lock.lock();
if (flag) {
bar.await();
}
// printBar.run() outputs "bar". Do not change or remove this line.
printBar.run();
flag = true;
foo.signal();
lock.unlock();
}
}
}
耗时和所占空间如下:
执行用时:23 ms
内存消耗:39.8 MB
关于这道题其实还有很多丰富的解法,还可以采用重入锁里头的信号量Semaphore进行实现,也可以采用AtomicInteger原子类进行乐观锁的实现,不过运行效果也比较差强人意,跟第一种方法自旋的时间和空间花费基本一致。
也可以借助synchronize实现,如果不想使用重入锁里面的signal和awati方法,亦可采用object里头的wait和notify方法。
也有小机灵鬼,直接采用同步阻塞队列,申明一个空间为1的队列,利用put和add的特性,put时候,如果空间不够用,会进入条件队列进行等待,take的时如果没有东西可取,同样也进入条件队列进行等待:
执行用时:
22 ms
内存消耗:
40.2 MB
private BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>(1);
public void foo(Runnable printFoo) throws InterruptedException {
for (int i = 0; i < n; i++) {
queue.put(true);
printFoo.run();
queue.put(true);
}
}
public void bar(Runnable printBar) throws InterruptedException {
for (int i = 0; i < n; i++) {
queue.take();
printBar.run();
queue.take();
}
}