这一个月,从C#转到java了,去年9月开始自学Java,还好,转成了,离想做的又近了一步,该继续写博客了
条件队列装入的数据项是等待先验条件成立而被挂起的线程。
我们想在得到的消息到来时,这个消息能立即得到处理,在大多数时候,我们的处理方式是在条件不满足时,让这个线程做自旋操作,如下:
while(!message)
{
Thread.yield();
}
这种处理方式可行,当另一线程带来了当前自旋线程需要得到的消息,也即message被打上了标志,当前线程退出while循环体继续之后的逻辑。但这样会造成当前时间片内CPU空转,即使使用了thread.yield()来让渡,但也只是减缓对CPU,上下文切换的消耗。
但有些时候我们是不能使用Thread.yield()或者Thread.sleep()的,因为我们可能会错过消息,当线程在主动休眠或者让渡期间,到达的两个消息(true-false),后到达的false会重写message中到达的ture,程序只会看到false,先到达的线程正在等待的先验条件丢失了。
并发情况下,使用while来等待我们需要的消息会带来很多问题,不让渡或者休眠会浪费CPU时间片,让渡或者休眠可能丢失正在等待的先验条件。我们可以使用条件队列解决这个问题。
正如每个对象都能当做一个锁,每个对象也能当做一个条件队列,对象中的wait,notify,notifyAll方法构成了内部条件队列的API,当对象调用wait方法时,当前线程会释放获得的对象锁,同时当前对象会请求操作系统挂起当前线程,此时对象的对象锁就可用了,允许其余等待线程进入,用《JAVA并发编程实践》的解释,线程在等待先验条件以期继续向后执行,在上面的示例中线程等待的先验条件就是:
message == true;
等待过程,用不考虑性能的方式,用while来完成。
更好的方式,用条件队列来完成,对象请求操作系统挂起当前线程的是为了减少对CPU的无谓消耗,对象释放对象锁的是为了能让其他线程进入当前对象,让其他线程进入当前对象是为了能有机会让先验条件成立,使用一个例子来解释:
开10个线程去竞争list中的第一个成员数据,当list中没有数据时,所有拿到了list对象锁的线程被挂起,对象锁被释放等待其他线程进入,寄希望于同一对象中的其他方法能带来阻塞线程等待的先验条件,在这个例子中这个方法是add(),wait,notify,notifyAll或者await,signal,signalAll,都需要获得当前对象的对象锁后才能继续操作,如此在例子中wait和notifyAll都放在了synchronized块中以保证在执行这几个API前对象锁已拿到,否则将会抛出java.lang.IllegalMonitorStateException,由于使用了notifyAll唤醒所有在条件队列中等到先验条件发生的线程,因此对于被过早唤醒的线程需要二次检查其等待的先验条件是否真的到达了。
最终执行效果:
对于notify,notifyAll两个方法,notify()只能随机唤醒条件队列中的某一个线程,这样会造成一个问题,线程1和线程2等待的不是同一个条件,当线程1被唤醒后,得到的消息却是线程2需要的消息,这称为信号丢失,线程仍在等待一个已经发生过的信号,对于notifyAll(),其会唤醒条件队列中的所有挂起线程,但需要确认,每一个线程等待的信号是否相同,如果等待的不同的信号,那么只能是先验条件达成的线程能继续向后执行,其余被过早唤醒的线程应该被继续挂起,因此大多数时候,我们需要对wait()方法的调用加上while(condition),即二次判断,判断被唤醒的线程所等待的信号是不是真实到达了,否则意外被唤醒的线程继续运行将带来未预料的程序错误。
不管是object的wait,notify,notifyAll,还是Conditon的await,signal,signalAll,调用的时候,都需要满足,当前线程已经获取到了当前对象的锁,来制造一个java.lang.IllegalMonitorStateException的异常:
代码里只改动了一行,即将list.notifyAll()改为了notifyAll();
按道理说我们已经使用了synchronized来保证进入块的当前线程获得了对象锁,这个时候调用notifyAll()是不改出现java.lang.IllegalMonitorStateException异常的。但这里出现了,我们要确保一点notifyAll()方法唤醒的哪一个对象的内部条件队列中的所有等待线程,在这里,使用notifyAll(),相当于:
synchronized(list)
{
list.add(6666);
this.notifyAll(); 即Main.notifyAll();
}
但我们需要唤醒的是:
list.notifyAll();
因为没有使用synchronized(Main)保证进入了synchronized(list)的线程同时也拿到了Main对象的对象锁,因此对Mian对象调用notifyAll()方法时出现了java.lang.IllegalMonitorStateException。
这是由于wait和notifyAll作用的不是同一个对象造成的异常情况。
一般情况下,wait和notifyAll配套作用于同一个对象不会造成问题,但有一个例外情况,在并发编程网的一篇文章:线程通信中提到了这种情况,本该作用于一个类的多个实例的wait,notify方法,由于字符串常量,单例模式,static修饰,造成本该只作用于一个实例范围类的notifyAll,实际却作用在了多个的”实例”上,这里打上引号的原因是,本认为是多个实例,由于字符串常量,单例模式,static修饰的原因实际作用于全局同一个对象实例上。
从这点来说单例模式,static修饰比较好理解,需要解释的是字符创常量,在JVM中,字符串常量是保存在字符串常量池中的,在JDK8之前,字符串常量池位于方法区中(也称为永久代),到JDK8时,其被移到了Heap中,对于两个字符串常量,比如:
str1="11";
str2="11";
“11”这个字符串会被保存在字符串常量池中,当需要新创建字符串”11”的实例时,如果字符串常量池中已有了这个字符串,那么就会直接使用这个字符串而不是重新创建,因此这里str1和str2是指向了同一个内存地址,也即本看上去是两个字符串是字符串实例的对象实际作用于全局同一个对象实例上,这样当str1上调用了notifyAll,却把str2对象上等待先验条件的对象唤醒,这个问题可以用二次检查解决,但如果是使用的是notify,这个时候使用notify是很正常的选择,由于随机唤醒,那么就可能去唤醒了str2对象上的线程,本该被唤醒的str1对象上的线程却仍在等待,信号丢失。