多线程篇5 线程的同步与共享

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

1.资源冲突

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

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的对象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关键字,如下所示:
synchronized void increase(){
	x++;
	y++;
}
synchronized void testEqual(){
	System.out.println(x+","+y+":"+(x==y)+":"+(x<y));
}

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

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

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

(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.foo()),调用线程必须获得X类的类锁。

3.线程间的同步控制

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

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

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

3.1不同步点设计

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

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对象调用CubbyHole的get()方法时,它也锁定该对象,防止Producer对象调用put()方法。

接下来我们看Producer和Consumer的定义,这两个类的定义如下:
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类的定义:

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()方法中也是一个循环,每次调用cubbyhole的get()方法返回当前存储的整数,然后输出。

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

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
…

3.2监视器模型

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

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用来指示内容是否可取。当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代码段中。它们的定义格式如下:

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

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

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

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

•	public final void notify()
•	public final void notifyAll()

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

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



你可能感兴趣的:(thread,多线程,线程,线程安全,异步)