一道关于synchronized和wait的面试题


今天看到一个笔试题,刚开始竟然有点蒙。经过一番摸索之后才发现问题所在,特此记录下来。

1.面试题

下面的代码在绝大部分时间内都运行得很正常,请问在什么情况下会出现问题?问题的根源在哪里?

  import java.util.LinkedList;

  public class Stack {
    LinkedList list = new LinkedList();
    public synchronized void push(Object x) {
      synchronized(list) {
        list.addLast( x );
        notify();
      }
    }
    public synchronized Object pop()
        throws Exception {
      synchronized(list) {
        if( list.size() <= 0 ) {
          wait();
        }
        return list.removeLast();
      }
    }
  }

猛地一看,这是不是脑子有坑啊?加这么多重量级锁。没办法,咱管不了,只能分析这个代码会出现什么问题。

2.分析

每个方法都加了一把锁,方法内还对list这个对象加了锁。如果按照正常的push、pop顺序来执行的话也没啥问题。那就从list的边界值开始测试吧。在没有值的情况下,先pop,再push。设计如下代码:

public static void main(String[] args) throws InterruptedException {

        Stack myStack = new Stack();
        Thread t1 = new Thread(() -> {
            try {
                myStack.pop();
            } catch (Exception e) {
                e.printStackTrace();
            }

        });
        t1.start();
        //确保t1线程首先执行
        Thread.sleep(1000);
        myStack.push(1);
    }

当断点跟着以上测试代码运行的时候,直接测出问题了:push()方法中的list.addLast( x );这一行根本进不去,也就是说代码停在了synchronized(list)这一行,在该代码块中获取list对象的锁时失败了。
为什么获取list对象的锁失败呢?因为pop()方法中的list锁没被释放。为什么没被释放呢?接下来得看看wait()这个方法,jdk8中的解释如下:

  • wait()
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.

翻译过来就是:使得当前的线程等待直到另一个线程对该对象调用了notify()或者notifyAll()方法.

  • notify()

方法notify()的解释如下:

Wakes up a single thread that is waiting on this object's monitor.

翻译过来就是:唤醒一个在当前对象的monitor中进行等待的线程

另外我们需要知道wait()方法是必须跟synchronized进行配套使用,否则会出现IllegalMonitorStateException的异常。而synchronized不同的使用方式,也需要wait()使用不同的方式进行等待。

  • synchronized锁定在普通方法上
    这个相当于锁定了该方法所在类的对象实例上,在该方法中使用wait()或者this.wait()均可进行释放锁和线程暂停。

  • synchronized锁定在静态方法上
    这个相当于锁定了该方法所在的类,在该方法中需要使用 类名.class.wait()(在本例中即为Stack.class.wait())的方式来释放锁进行线程暂停。

  • synchronized锁定在指定对象上
    这个相当于锁定了指定对象,需要使用 对象.wait()(在本例中即为list.wait())来释放锁并暂停线程。

当我们在pop()方法中首先把list进行了锁定,然后直接调用wait()方法时,表示的是调用Stack这个类的实例对象所在的线程进行了等待,也就是测试代码中t1线程阻塞住了,但并没有把对象list解锁。

当1秒后执行到main线程中的myStack.push(1);这一行代码时,push()方法中首先尝试锁定list,但由于被t1线程给锁定后并没有释放,导致这里获取锁失败,故而卡在这里动不了,最终造成了死锁。

3.解决方法

既然找到了原因,那就好说了。pop()push()方法及方法体中均有两层锁,我们需要去掉一层。

  • 方法1: 去掉方法上的synchronized
public class Stack {
    LinkedList list = new LinkedList();
    public void push(Object x) {
        synchronized(list) {
            list.addLast( x );
            list.notify();
        }
    }

    public Object pop() throws Exception {
        synchronized(list) {
            if ( list.size() <= 0 ) {
                list.wait();
            }
            return list.removeLast();
        }
    }
}
  • 方法2: 去掉list对象上的锁
public class Stack {
    LinkedList list = new LinkedList();

    public synchronized void push(Object x) {
        list.addLast(x);
        this.notify();
    }

    public synchronized Object pop() throws Exception {
        if (list.size() <= 0) {
            this.wait();
        }
        return list.removeLast();
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值