第二节、线程同步
当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用。达到此目的的过程叫做同步(synchronization)。Java中使用synchronized关键字来修饰需要同步的方法或对象。Java中每个对象和类都有一个监视器(monitor)。线程可以暂时拥有监视器的使用权,过些时候再释放(以便另一个线程可以享有该监视器的使用权)。就象线程间的同步一样重要,因为一个监视器在同一时间只能有一个线程可以使用。单个线程可以获得多个监视器;一个线程获得了对象的监视器后可调用该对象的其他同步方法(这是另外一个监视器);线程的任何状态都能够调用非同步方法;内部类对象中的监视器和外部类对象中的监视器是完全独立的,即获得一个外部类对象的监视器不意味着也获得内部类的监视器。(不知道使用监视器是否正确,还是应该使用锁,有待查清)
实例:
package Examples;
public class SyncThreadDemo{
public static void main(String[] args){
CallMe cm1=new CallMe();
Caller c1=new Caller(cm1,"A ");
Caller c2=new Caller(cm1,"Hello World");
Caller c3=new Caller(cm1,"Sample!");
try{
c1.t.join();
c2.t.join();
c3.t.join();
}catch(InterruptedException e){
System.out.println("Main thread interrupted!");
}
}
}
class CallMe{
void call(String msg){ //可将call函数声明为synchronized的,这样调用它的线程就不
// 必在调用时将它所在类包含在synchronized块内。
System.out.print("["+msg);
try{
Thread.sleep(1500);
}catch(InterruptedException e){
System.out.println("CallMe interrupeted!");
}
System.out.println("]");
}
}
class Caller implements Runnable{
String msg;
CallMe tarCM;
Thread t;
public Caller(CallMe cm,String s){
tarCM=cm;
msg=s;
t=new Thread(this);
t.start();
}
public void run(){
synchronized(tarCM){
tarCM.call(msg);
}
}
}
第三节、线程间通信
Java中的同步消除了两个或多个线程同时访问共享资源而发生的并发错误。但是,就线程间通信而言,同步不起任何作用。有时你需要一种方法告诉一个线程另一个线程正在做什么。Java通过使用wait()、notify()和notifyAll()方法来实现。
wait() :告知被调用的线程放弃监视器进入等待状况,直到其他线程进入相同监视器并调用notify()或notifyAll()方法。
notify() :恢复相同对象中第一调用wait()的线程
notifyAll():恢复相同对象中所有调用wait()的线程。具有最高优先级的线程最先运行。
它们的声明如下:
final void wait() throws InterruptedException
final void notify()
final void notifyAll()
wait()方法还存在允许你定义等待时间的函数。
实例分析: 以下实例中有4个类,Q类是共享资源,有两个同步方法put()和set(),put()方法设置Q中的n变量,set()方法读取Q中的同个n变量。Producer类是一个线程,不停的调用Q类的一个实例中的put()方法,Consumer类也是一个线程,它则不停的调用同一个Q类的实例中的set()方法。ThreadCommunicate类则是使用上面三个类的示例,示范了如何在Producer实例put一次,Consumer才能get一次,Consumer进行get后Producer才能再put,这样一直循环下去,直到你按“Ctrl+C”结束,主要是通过设置一个boolean变量来检测已put或已get状态。由于输出很快,所以我们在put和get方法中都使用了Thread.sleep(1000); ,即让每次操作后该线程都休眠1秒,这样可以清楚看到每次put和get的数,示例代码如下:
package Examples;
class Q{
private int n;
//valueSet初始化为false,
//当调用put()方法设置变量n的值后,valueSet值为true
//当调用get()方法获得变量n的值时,valueSet值为false
private boolean valueSet=false;
synchronized void put(int n){
if(valueSet){ //如果valueSet为true,说明新设置的n的值还没被获取
//必须让线程等待,直到其他线程调用notify()方法
try{
wait();
}catch(InterruptedException e){
System.out.println("Interrupted when put!");
}
}
this.n=n;
valueSet=true;
System.out.println("Put: "+n);
notify(); //唤醒正在等待的另一个线程
}
synchronized int get(){
if(!valueSet){ //如果valueSet为false,说明获取n的值后,n还没被重新设置
//必须让线程等待,直到其他线程调用notify()方法
try{
wait();
}catch(InterruptedException e){
System.out.println("Interrupted when get!");
}
}
System.out.println("Got: "+n);
valueSet=false;
notify(); //唤醒正在等待的另一个线程
return n;
}
}
//创建一个叫Producer的线程,该线程调用一个Q类的实例,并使用while循环不停地调用该实例的put方法
class Producer implements Runnable{
Q q;
Producer(Q q){
this.q=q;
new Thread(this,"Producer").start();
}
public void run(){
int i=0;
try{
while(true){
q.put(i++);
Thread.sleep(1000);
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
//创建一个叫Consumer的线程,该线程调用一个Q类的实例,并使用while循环不停地调用该实例的get方法
class Consumer implements Runnable{
Q q;
Consumer(Q q){
this.q=q;
new Thread(this,"Consumer").start();
}
public void run(){
try{
while(true){
q.get();
Thread.sleep(1000);
}
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
public class ThreadCommunicate{
public static void main(String[] args){
Q q=new Q();
new Producer(q);
new Consumer(q);
System.out.println("Press Control-C to stop.");
}
}
你可以尝试一下去除 valueSet变量相关语句,和sleep()语句后再编译执行看是怎样的情况。
第四节、死锁
需要避免的与多任务处理有关的特殊错误类型是死锁(deadlock)。死锁发生在当两个线程对一对同步对象有循环依赖关系时。死锁是很难调试的错误,因为:
* 通常它极少发生,只有到两线程的时间段刚好符合时才能发生
* 它可能包含多于两个的线程和同步对象
实例分析: 以下实例有三个类,类A和类B形式和功能相同,都是具有两个同步方法,first()和last(),A的first()方法会执行一个打印语句,接着休眠然后再调用B对象的last()方法,B的first()方法类似,不同的只是它调用A的last方法。
类DeadLock实现了Runnable接口,在创建一个DeadLock对象的同时启动新的线程(新的线程调用了B对象的first()方法),并随即调用A对象的first()方法。新的线程调用B对象的first()方法,获得B的监视器后休眠1秒(此时在B的first()方法内还未返回)。在这段时间里程序继续执行,主线程(使用DeadLock对象的线程)调用A对象的first()方法,获得了A的监视器后也休眠了(在A的first()方法内还未返回),当新线程恢复执行时,它要调用A对象的last()方法,但A对象被主线程占用,必须等待,随即主线程也恢复执行,它则要调用B对象的last()方法,此时就出现了死锁。(好像说明得有些乱。。。)
示例代码如下:
package Examples;
class A{
synchronized void first(B b){
String name=Thread.currentThread().getName();
System.out.println(name+" entered A.first()");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println("A interrupted!");
}
System.out.println(name+" trying to call B.last()...");
b.last();
}
synchronized void last(){
System.out.println("A/'s last");
}
}
class B{
synchronized void first(A a){
String name=Thread.currentThread().getName();
System.out.println(name+" entered B.first()");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println("B interrupted!");
}
System.out.println(name+" trying to call A.last()...");
a.last();
}
synchronized void last(){
System.out.println("B/'s last");
}
}
public class DeadLock implements Runnable{
A a=new A();
B b=new B();
DeadLock(){
Thread.currentThread().setName("MainThread");
Thread t=new Thread(this,"RacingThread");
t.start(); //启动新的线程
a.first(b); //此处和新的线程并发执行。
// 调用a的first()方法,获得a的监视器,方法中代码的执行:
// 先休眠1秒再调用b的last()方法,此时出现死锁
System.out.println("Back in main thread");
}
public void run(){
//在此处添加后面注释中的代码可避免死锁
b.first(a); //调用b的first()方法,获得b的监视器,方法中的代码执行:
//先休眠1秒再调用a的last()方法,此时出现死锁
System.out.println("Back in main thread");
}
public static void main(String[] args){
new DeadLock();
}
}
注释:
try{
Thread.sleep(1000);
}catch(InterruptedException e){
System.out.println("Interrupeted!");
}
第五节、挂起、恢复和终止线程
挂起、恢复和终止线程机制在Java2和早期版本中有所不同。Java2前的版本,程序用Thread定义的suspend()、resume()和stop()来实现。但在Java2中,它们不能使用,如果使用会出现编译错误,提示这些方法不赞成使用。因为suspend()方法有时会造成严重的系统故障。假定对关键的数据结构的一个线程被锁定的情况下,如果该线程在那里挂起,这些锁定的线程并没有放弃对资源的控制,其他的等待这些资源的线程可能死锁。resume()方法同样不被赞成使用,它不引起问题,但不能离开suspend()方法单独使用。stop()方法同样受到反对,设想一个线程正在写一个精密的重要的数据结构且仅完成一个零头,如果该线程在此刻终止,则数据结构可能会停留在崩溃状态。
Java2中,线程必须被设计以使run()方法定期检查来判定线程是否应该被挂起,恢复或终止它自己的执行。
实例分析:NewThread类中包含了用来控制线程执行的布尔型变量suspendFlag,它被构造器初始化为false。run()方法包含一个监测suspendFlag的同步声明的块。如果变量是true,wait()方法被调用以挂起线程。mySuspend()方法设置suspendFlag为true,myResume()方法设置suspendFlag为false并且调用notify()方法来唤起线程。
package Examples;
class NewThread implements Runnable{
String name;
Thread t;
boolean suspendFlag;
NewThread(String threadName){
name=threadName;
t=new Thread(this,name);
System.out.println("New Thread: "+t);
suspendFlag=false;
t.start();
}
public void run(){
try{
for(int i=5;i>0;i--){
synchronized(this){
while(suspendFlag){
wait();
}
System.out.println(name+": "+i);
Thread.sleep(200);
}
}
}catch(InterruptedException e){
System.out.println(name+" interrupted!");
}
System.out.println(name+" exiting!");
}
void mySuspend(){
suspendFlag=true;
}
synchronized void myResume(){
suspendFlag=false;
notify();
}
}
public class NewThreadDemo{
public static void main(String[] args){
NewThread nt1=new NewThread("One");
NewThread nt2=new NewThread("Two");
try{
Thread.sleep(200);
nt1.mySuspend();
System.out.println("Thread One is suspend.");
Thread.sleep(400);
nt1.myResume();
System.out.println("Thread One is resume.");
nt2.mySuspend();
System.out.println("Thread Two is suspend.");
Thread.sleep(1000);
nt2.myResume();
System.out.println("Thread Two is resume.");
}catch(InterruptedException e){
System.out.println("Main thread interrupted!");
}
//wait for threads to finish
try{
System.out.println("Waiting for threads to finish.");
nt1.t.join();
nt2.t.join();
}catch(InterruptedException e){
System.out.println("Main thread interrupted!");
}
System.out.println("Main thread exiting.");
}
}