java something

不要以为......很遥远
随笔 - 23, 文章 - 1, 评论 - 2, 引用 - 0
数据加载中……

线程的同步与共享

线程的同步与共享

前面程序中的线程都是独立的、异步执行的线程。但在很多情况下,多个线程需要共享数据资源,这就涉及到线程的同步与资源共享的问题。

资源冲突

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

程序CounterTest.java

 

class Num
{
 private int x = 0;
 private int y = 0;

 void increase()
 {
  x++;
  y++;
 }

 void testEqual()
 {
  System.out.println(x + "," + y + ":" + (x == y));
 }
}

class Counter extends Thread
{
 private Num num;

 Counter(Num num)
 {
  this.num = num;
 }

 public void run()
 {
  while (true)
  {
   num.increase();
  }
 }
}

public class CounterTest
{
 public static void main(String[] args)
 {
  Num num = new Num();
  Thread count1 = new Counter(num);
  Thread count2 = new Counter(num);
  count1.start();
  count2.start();

  for (int i = 0; i < 100; i++)
  {
   num.testEqual();
   try
   {
    Thread.sleep(100);
   } catch (InterruptedException e)
   {
   }
  }
 }
}

 

 

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

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

对象锁的实现

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

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

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

对于上面的程序我们可以在定义Num类的increase()testEqual()方法时,在它们前面加上synchronized关键字,如下所示:

synchronized void increase(){

    x++;

    y++;

}

synchronized void testEqual(){

    System.out.println(x+","+y+":"+(x==y)+":"+(x<y));

}

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

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

如果只为increase()方法添加synchronized 关键字,结果还会出现xy的值不相等的情况.
    
    (2)
前面实现对象锁是在方法前加上synchronized 关键字,这对于我们自己定义的类很容易实现,但如果使用类库中的类或别人定义的类在调用一个没有使用synchronized关键字修饰的方法时,又要获得对象锁,可以使用下面的格式:

synchronized(object){

   //方法调用

}

假如Num类的increase()方法没有使用synchronized 关键字,我们在定义Counter类的run()方法时可以按如下方法使用synchronized为部分代码加锁。

public void run(){

    while(true){

synchronized (num){

       num.increase();

     }

    }

}

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

synchronized(num){

    num.testEqual();

}

对象锁的获得和释放是由Java运行时系统自动完成的。

每个类也可以有类锁。类锁控制对类的synchronized static代码的访问。请看下面的例子:

public class X{

 static int x, y;

 static synchronized void foo(){

     x++;

y++;

}

}

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

3  线程间的同步控制

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

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

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

1. 不同步的设计

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

程序 CubbyHole.java

class CubbyHole{

  private int content ;

public synchronized void put(int value){

content = value;

}

  public synchronized int get(){

return content ;

}

}

_____________________________________________________________________________

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

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

接下来我们看ProducerConsumer的定义,这两个类的定义如下:

程序 Producer.java

public class Producer extends Thread {

    private CubbyHole cubbyhole;

    private int number;

    public Producer(CubbyHole c, int number) {

        cubbyhole = c;

        this.number = number;

    }

    public void run() {

       for (int i = 0; i < 10; i++) {

          cubbyhole.put(i);

          System.out.println("Producer #" + this.number + " put: " + i);

          try {

                sleep((int)(Math.random() * 100));

           } catch (InterruptedException e) { }

        }

    }

}

_____________________________________________________________________________

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

下面是Consumer类的定义:

程序 Consumer.java

public class Consumer extends Thread {

    private CubbyHole cubbyhole;

    private int number;

    public Consumer(CubbyHole c, int number) {

        cubbyhole = c;

        this.number = number;

    }

    public void run() {

        int value = 0;

        for (int i = 0; i < 10; i++) {

            value = cubbyhole.get();

      System.out.println("Consumer #" + this.number + " got: " + value);

        }

    }

}

_____________________________________________________________________________

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

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

程序 ProducerConsumerTest.java

public class ProducerConsumerTest {

    public static void main(String[] args) {

        CubbyHole c = new CubbyHole();

        Producer p1 = new Producer(c, 1);

        Consumer c1 = new Consumer(c, 1);

        p1.start();

        c1.start();

    }

}

_____________________________________________________________________________

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

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

Consumer: 3

Producer: 4

Producer: 5

Consumer: 5

反之,如果消费者比生产者快,消费者可能两次取同一个数据,可能产生下面的结果:

Producer: 4

Consumer: 4

Consumer: 4

Producer: 5

2. 监视器模型

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

下面是修改后的CubbyHole类的定义:

程序CubbyHole.java

class CubbyHole{

  private int content ;

  private boolean available=false;

 

public synchronized void put(int value){

 while(available==true){

      try{

wait();

}catch(InterruptedException e){}

}

content =value;

available=true;

notifyAll();

}

  public synchronized int get(){

    while(available==false){

      try{

wait();

}catch(InterruptedException e){}

    }

available=false;

notifyAll();

return content;

}

}

_____________________________________________________________________________

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

当生产者线程进入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代码段中。它们的定义格式如下:

·         public final void wait()

·         public final void wait(long timeout)

·         public final void wait(long timeout, int nanos)

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

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

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

·         public final void notify()

·         public final void notifyAll()

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

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








 

posted on 2011-09-02 01:38 Jamie 阅读(455) 评论(0)  编辑  收藏 所属分类: 多线程


只有注册用户登录后才能发表评论。


网站导航: