多线程

1.资源冲突

下面的例子说明,多个线程共享资源,如果不加以控制可能会产生冲突。

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. class Num{  
  2.     private int x=0;  
  3.     private int y=0;  
  4.     void increase(){  
  5.         x++;  
  6.         y++;  
  7.         }  
  8.     void testEqual(){  
  9.         System.out.println(x+","+y+":"+(x==y));  
  10.     }  
  11. }  
  12. class Counter extends Thread{  
  13. private Num num;  
  14. Counter(Num num){  
  15.   this.num=num;   
  16. }  
  17. public void run(){  
  18. while(true){  
  19.   num.increase();  
  20. }  
  21. }  
  22. }  
  23. public class CounterTest{  
  24. public static void main(String[] args){  
  25.     Num num = new Num();  
  26.     Thread count1 = new Counter(num);  
  27.     Thread count2 = new Counter(num);  
  28.     count1.start();  
  29.     count2.start();  
  30.     for(int i=0;i<100;i++){  
  31.       num.testEqual();  
  32.         try{  
  33.           Thread.sleep(100);  
  34.         }catch(InterruptedException e){ }  
  35.     }  
  36.    }  
  37. }  

上述程序在CounterTest类的main()方法中创建了两个线程类Counter的对象count1和count2,这两个对象共享一个Num类的对象num。两个线程对象开始运行后,都调用同一个对象num的increase()方法来增加num对象的x和y的值。在main()方法的for()循环中输出num对象的x和y的值。程序输出结果有些x、y的值相等,大部分x、y的值不相等。

出现上述情况的原因是:两个线程对象同时操作一个num对象的同一段代码,通常将这段代码段称为临界区(criticalsections)。在线程执行时,可能一个线程执行了x++语句而尚未执行y++语句时,系统调度另一个线程对象执行x++和y++,这时在主线程中调用testEqual()方法输出x、y的值不相等。

这里可能出现x的值小于y的值的情况,为什么?

2.对象锁的实现

上述程序的运行结果说明了多个线程访问同一个对象出现了冲突,为了保证运行结果正确(x、y的值总相等),可以使用Java语言的synchronized关键字,用该关键字修饰方法。用synchronized关键字修饰的方法称为同步方法,Java平台为每个具有synchronized代码段的对象关联一个对象锁(object lock)。这样任何线程在访问对象的同步方法时,首先必须获得对象锁,然后才能进入synchronized方法,这时其他线程就不能再同时访问该对象的同步方法了(包括其他的同步方法)。

通常有两种方法实现对象锁:

(1) 在方法的声明中使用synchronized关键字,表明该方法为同步方法。

对于上面的程序我们可以在定义Num类的increase()和testEqual()方法时,在它们前面加上synchronized关键字,如下所示:
[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. synchronized void increase(){  
  2.     x++;  
  3.     y++;  
  4. }  
  5. synchronized void testEqual(){  
  6.     System.out.println(x+","+y+":"+(x==y)+":"+(x<y));  
  7. }  

一个方法使用synchronized关键字修饰后,当一个线程调用该方法时,必须先获得对象锁,只有在获得对象锁以后才能进入synchronized方法。一个时刻对象锁只能被一个线程持有。如果对象锁正在被一个线程持有,其他线程就不能获得该对象锁,其他线程就必须等待持有该对象锁的线程释放锁。

如果类的方法使用了synchronized关键字修饰,则称该类对象是线程安全的,否则是线程不安全的。

如果只为increase()方法添加synchronized 关键字,结果还会出现x、y的值不相等的情况,请考虑为什么?

(2) 前面实现对象锁是在方法前加上synchronized 关键字,这对于我们自己定义的类很容易实现,但如果使用类库中的类或别人定义的类在调用一个没有使用synchronized关键字修饰的方法时,又要获得对象锁,可以使用下面的格式:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. synchronized(object){  
  2.    //方法调用  
  3. }  
假如Num类的increase()方法没有使用synchronized 关键字,我们在定义Counter类的run()方法时可以按如下方法使用synchronized为部分代码加锁。
[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public void run(){  
  2.     while(true){  
  3. synchronized (num){  
  4.         num.increase();  
  5.       }  
  6.     }}  

同时在main()方法中调用testEqual()方法也用synchronized关键字修饰,这样得到的结果相同。

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. synchronized(num){  
  2.     num.testEqual();  
  3. }  
对象锁的获得和释放是由Java运行时系统自动完成的。每个类也可以有类锁。类锁控制对类的synchronized static代码的访问。请看下面的例子:
[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public class X{  
  2.   static int x, y;  
  3.   static synchronized void foo(){  
  4.      x++;  
  5. y++;  
  6. }  

当foo()方法被调用时(如,使用X.foo()),调用线程必须获得X类的类锁。

3.线程间的同步控制

在多线程的程序中,除了要防止资源冲突外,有时还要保证线程的同步。下面通过生产者-消费者模型来说明线程的同步与资源共享的问题。

假设有一个生产者(Producer),一个消费者(Consumer)。生产者产生0~9的整数,将它们存储在仓库(CubbyHole)的对象中并打印出这些数来;消费者从仓库中取出这些整数并将其也打印出来。同时要求生产者产生一个数字,消费者取得一个数字,这就涉及到两个线程的同步问题。

这个问题就可以通过两个线程实现生产者和消费者,它们共享CubbyHole一个对象。如果不加控制就得不到预期的结果。

3.1不同步点设计

首先我们设计用于存储数据的类,该类的定义如下:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. class CubbyHole{  
  2.   private int content ;  
  3. public synchronized void put(int value){  
  4. content = value;  
  5. }  
  6.   public synchronized int get(){  
  7. return content ;  
  8. }   
  9. }  

CubbyHole类使用一个私有成员变量content用来存放整数,put()方法和get()方法用来设置变量content的值。CubbyHole对象为共享资源,所以用synchronized关键字修饰。当put()方法或get()方法被调用时,线程即获得了对象锁,从而可以避免资源冲突。

这样当Producer对象调用put()方法是,它锁定了该对象,Consumer对象就不能调用get()方法。当put()方法返回时,Producer对象释放了CubbyHole的锁。类似地,当Consumer对象调用CubbyHole的get()方法时,它也锁定该对象,防止Producer对象调用put()方法。

接下来我们看Producer和Consumer的定义,这两个类的定义如下:
[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public class Producer extends Thread {  
  2.     private CubbyHole cubbyhole;  
  3.     private int number;  
  4.     public Producer(CubbyHole c, int number) {  
  5.         cubbyhole = c;  
  6.         this.number = number;  
  7.     }  
  8.     public void run() {  
  9.        for (int i = 0; i < 10; i++) {  
  10.           cubbyhole.put(i);  
  11.           System.out.println("Producer #" + this.number + " put: " + i);  
  12.           try {  
  13.                 sleep((int)(Math.random() * 100));  
  14.            } catch (InterruptedException e) { }  
  15.         }  
  16.     }  
  17. }  

Producer类中定义了一个CubbyHole类型的成员变量cubbyhole,它用来存储产生的整数,另一个成员变量number用来记录线程号。这两个变量通过构造方法传递得到。在该类的run()方法中,通过一个循环产生10个整数,每次产生一个整数,调用cubbyhole对象的put()方法将其存入该对象中,同时输出该数。下面是Consumer类的定义:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public class Consumer extends Thread {  
  2.     private CubbyHole cubbyhole;  
  3.     private int number;  
  4.     public Consumer(CubbyHole c, int number) {  
  5.         cubbyhole = c;  
  6.         this.number = number;  
  7.     }  
  8.     public void run() {  
  9.         int value = 0;  
  10.         for (int i = 0; i < 10; i++) {  
  11.             value = cubbyhole.get();  
  12.       System.out.println("Consumer #" + this.number + " got: " + value);  
  13.         }  
  14.     }  
  15. }  

在Consumer类的run()方法中也是一个循环,每次调用cubbyhole的get()方法返回当前存储的整数,然后输出。

下面是主程序,在该程序的main()方法中创建一个CubbyHole对象c,一个Producer对象p1,一个Consumer对象c1,然后启动两个线程。

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. public class ProducerConsumerTest {  
  2.     public static void main(String[] args) {  
  3.         CubbyHole c = new CubbyHole();  
  4.         Producer p1 = new Producer(c, 1);  
  5.         Consumer c1 = new Consumer(c, 1);  
  6.         p1.start();  
  7.         c1.start();  
  8.     }  
  9. }  

该程序中对CubbyHole类的设计,尽管使用了synchronized关键字实现了对象锁,但这还不够。程序运行可能出现下面两种情况:

如果生产者的速度比消费者快,那么在消费者来不及取前一个数据之前,生产者又产生了新的数据,于是消费者很可能会跳过前一个数据,这样就会产生下面的结果:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. Consumer: 3  
  2. Producer: 4  
  3. Producer: 5  
  4. Consumer: 5  
  5. …  
反之,如果消费者比生产者快,消费者可能两次取同一个数据,可能产生下面的结果

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. Producer: 4  
  2. Consumer: 4  
  3. Consumer: 4  
  4. Producer: 5  
  5. …  

3.2监视器模型

为了避免上述情况发生,就必须使生产者线程向CubbyHole对象中存储数据与消费者线程从CubbyHole对象中取得数据同步起来。为了达到这一目的,在程序中可以采用监视器(monitor)模型,同时通过调用对象的wait()方法和notify()方法实现同步。下面是修改后的CubbyHole类的定义:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. class CubbyHole{  
  2.   private int content ;  
  3.   private boolean available=false;  
  4. public synchronized void put(int value){  
  5.  while(available==true){  
  6.       try{  
  7. wait();  
  8. }catch(InterruptedException e){}  
  9. }   
  10. content =value;  
  11. available=true;  
  12. notifyAll();  
  13. }  
  14.   public synchronized int get(){  
  15.     while(available==false){  
  16.       try{  
  17. wait();  
  18. }catch(InterruptedException e){}  
  19.     }  
  20. available=false;  
  21. notifyAll();  
  22. return content;  
  23. }   
  24. }  

这里有一个boolean型的私有成员变量available用来指示内容是否可取。当available为true时表示数据已经产生还没被取走,当available为false时表示数据已被取走还没有存放新的数据。

当生产者线程进入put()方法时,首先检查available的值,若其为false,才可执行put()方法,若其为true,说明数据还没有被取走,该线程必须等待。因此在put()方法中调用CubbyHole对象的wait()方法等待。调用对象的wait()方法使线程进入等待状态,同时释放对象锁。直到另一个线程对象调用了notify()或notifyAll()方法,该线程才可恢复运行。

类似地,当消费者线程进入get()方法时,也是先检查available的值,若其为true,才可执行get()方法,若其为false,说明还没有数据,该线程必须等待。因此在get()方法中调用CubbyHole对象的wait()方法等待。调用对象的wait()方法使线程进入等待状态,同时释放对象锁。

上述过程就是监视器模型,其中CubbyHole对象为监视器。通过监视器模型可以保证生产者线程和消费者线程同步,结果正确。

程序的运行结果如下:

特别注意:wait()、notify()和notifyAll()方法是Object类定义的方法,并且这些方法只能用在synchronized代码段中。它们的定义格式如下:

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. •   public final void wait()  
  2. •   public final void wait(long timeout)  
  3. •   public final void wait(long timeout, int nanos)  

当前线程必须具有对象监视器的锁,当调用该方法时线程释放监视器的锁。调用这些方法使当前线程进入等待(阻塞)状态,直到另一个线程调用了该对象的notify()方法或notifyAll()方法,该线程重新进入运行状态,恢复执行。

timeout和nanos为等待的时间的毫秒和纳秒,当时间到或其他对象调用了该对象的notify()方法或notifyAll()方法,该线程重新进入运行状态,恢复执行。

wait()的声明抛出了InterruptedException,因此程序中必须捕获或声明抛出该异常。

[java]  view plain copy print ? 在CODE上查看代码片 派生到我的代码片
  1. •   public final void notify()  
  2. •   public final void notifyAll()  

唤醒处于等待该对象锁的一个或所有的线程继续执行,通常使用notifyAll()方法。

    在生产者/消费者的例子中,CubbyHole类的put和get方法就是临界区。当生产者修改它时,消费者不能问CubbyHole对象;当消费者取得值时,生产者也不能修改它。

package nahuadaxue;


class Queue{
private int value;
//定义一个拿的方法
 private boolean bFull=false;
public synchronized void put(int i){
while (bFull==true){


try {
wait();
} catch (Exception e) {
e.getStackTrace();
}
}
value=i;
bFull=true;
notifyAll();



}

public  synchronized int get(){
while(bFull==false){

try {
wait();
} catch (Exception e) {
// TODO: handle exception
}
}
bFull=false;
notifyAll();
return value;
}
}package nahuadaxue;


class Producer extends Thread{
Queue q;
//构造
Producer (Queue q){
this.q=q;
}
public void run(){
for(int i=0;i<10;i++){
q.put(i);
System.out.println("producer put"+i);  
}
}
}

package nahuadaxue;
                                                                                                                            consumer get0
producer put0
producer put1
consumer get1
producer put2
consumer get2
producer put3
consumer get3
consumer get4
producer put4
producer put5
consumer get5
producer put6
producer put7
consumer get6
consumer get7
consumer get8
producer put8
producer put9
consumer get9

class Consumer extends Thread{
Queue q;
//构造
Consumer (Queue q){
this.q=q;
}
public void run(){
while(true){
System.out.println("consumer get"+q.get());
 
}
}
}package nahuadaxue;


public class Test {
public static void main(String[] args) {
Queue q=new Queue();
Producer p=new Producer(q);
Consumer c=new Consumer(q);
p.start();
c.start();
}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值