对于初学者来说,下面这个例子是一个非常常见的错误。
/**
* 线程A: 循环50次后等待并放弃锁,让线程B执行。
*/
class ThreadA extends Thread{
//线程同步的公共数据区
Object oa=null;
ThreadA(Object o){
this.oa=o;
}
//线程A执行逻辑
public void run(){
//线程同步区域,需要申请公共数据的锁
synchronized(oa){
System.out.println("ThreadA is running......");
for(int i=0;i<100;i++){
System.out.println(" ThreadA value is "+i);
if(i==50){
try {
//当前线程等待
Thread.currentThread().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}//if(i==50)
}//for(int i)
}
}
}
/**
* 线程B:等待线程A放弃锁,然后获得锁并执行,完成后唤醒线程A
*/
class ThreadB extends Thread{
//线程同步的公共数据区
Object ob=null;
ThreadB(Object o){
this.ob=o;
}
//线程B执行逻辑
public void run(){
//线程同步区域,需要申请公共数据的锁
synchronized(ob){
System.out.println("ThreadB is running......");
for(int i=0;i<50;i++){
System.out.println(" ThreadB value is "+i);
}
//唤醒等待的线程
notify();
}
}
}
//测试
public class ThreadTest {
public static void main(String[] args){
Object lock=new Object(); //公共数据区
ThreadA threada=new ThreadA(lock);
ThreadB threadb=new ThreadB(lock);
threada.start(); //线程A执行
threadb.start(); //线程B执行
}
}
程序很简单,就是让线程A,B交替打印。但是运行的时候会抛出两个异常:
Exception in thread "Thread-0" java.lang.IllegalMonitorStateException: current thread not owner
Exception in thread "Thread-1" java.lang.IllegalMonitorStateException: current thread not owner
问题就处在ThreadA中的Thread.currentThread().wait(); 和ThreadB中的notify();上。
初学者理解wait()的时候都认为是将当前线程阻塞,所以Thread.currentThread().wairt();视乎很有道理。但是不知道大家有没有发现,在JDK类库中wait()和notify()方法并不是Thread类的,而是Object()中的。我们仔细看看wait方法的JDK文档:
public final void wait() throws InterruptedException
在其他线程调用此对象的 notify()
方法或 notifyAll()
方法前,当前线程等待。 换句话说,此方法的行为就好像它仅执行 wait(0)
当前线程必须拥有此 对象监视器 。该线程发布对此监视器的所有权并等待 ,直到其他线程通过调用 notify
方法,或 notifyAll
方法通知在此对象的监视器上等待的线程醒来。
对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:
synchronized (obj) {
while (<condition does not hold>)
obj.wait();
// Perform action appropriate to condition
}
此方法只应由作为此对象监视器的所有者的线程来调用。
抛出: IllegalMonitorStateException
- 如果当前线程不是此对象监视器的所有者。
InterruptedException
- 如果在当前线程等待通知之前或者正在等待通知时,任何线程中断了当前线程。在抛出此异常时,当前线程的中断状态 被清除。
Thread.currentThread().wait();改成oa.wait()notify();改成 ob.notify()就没有问题了。
同步块obj来调用wait/notify方法
首先,我们都知道JVM会给每一个对象都分配唯一的一把锁。这把锁是在对象中的。
然后,当Thread-0线程获得了这把锁后,应该是在对象中的锁内记录下当前占有自己的线程号,并把自己设置为已被占用。那么当Thread-0需要放弃锁的时候,锁对象会把 Thread-0放入到锁的等待队列中 。而这一切和Thread-0是没有任何关系的。自然也轮不到Thread-0对象来调用某个方法来改变另一个对象中的锁(这一点也说不通,我自己的锁凭什么让你来改)。
因此,也就出现用改变公共数据区对象的锁的方法是通过共数据区对象本省来调用,和线程对象是没有关系的。
事实上,每一个同步锁lock下面都挂了几个线程队列,包括就绪(Ready)队列,等待(Waiting)队列等。当线程A因为得不到同步锁lock,从而进入的是lock.ReadyQueue(就绪队列),一旦同步锁不被占用,JVM将自动运行就绪队列中的线程而不需要任何notify()的操作。但是当线程A被wait()了,那么将进入lock.WaitingQuene(等待队列),同时如果占据的同步锁也会放弃。而此时如果同步锁不唤醒等待队列中的进程(lock.notify()),这些进程将永远不会得到运行的机会。
就绪队列和等待队列有很大的不同,这一点要牢记。
对于object的wait、notify、notifyAll的理解,可以参见《Java 虚拟机体系结构 》中堆内存区的内容,就会有更加深刻的理解了。
看了一下线程这方面的知识,也结合了一些 面试的题 总结如下:
一些基础的就不说了,主要说一下这里面的几个方法,也是object的通用方法。
sleep() wait() notify/notifyAll() 的区别
sleep()是线程类的方法,sleep() 允许指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。说白了 ,也就是把机会给其他线程,但是监控状态依然保持。重要的一点就是 当调用sleep()方法是 不会 释放对象锁的。
下面详细讲一下 : wait() notify/notifyAll() 的区别
先来谈谈为什么所有的类中都有这一对方法,看是很奇怪,其实是 Thread类提供的,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。
notify()是释放对象的wait()方法而阻塞线程(但是也要当得到锁后才可以运行)但是这个释放是随机的,也就是不一定要释放那个线程。(因为调用同一资源的可能不是一个线程或者说是有多个阻塞的线程在等待,但是如果加了synchronized也只有一个线程,也有其他的线程在等待中,也就是阻塞)我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。
同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现 IllegalMonitorStateException 异常。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。