前面程序中的线程都是独立的、异步执行的线程。但在很多情况下,多个线程需要共享数据资源,这就涉及到线程的同步与资源共享的问题。
下面的例子说明,多个线程共享资源,如果不加以控制可能会产生冲突。
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的值的情况,为什么?上述程序的运行结果说明了多个线程访问同一个对象出现了冲突,为了保证运行结果正确(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类的类锁。
在多线程的程序中,除了要防止资源冲突外,有时还要保证线程的同步。下面通过生产者-消费者模型来说明线程的同步与资源共享的问题。
假设有一个生产者(Producer),一个消费者(Consumer)。生产者产生0~9的整数,将它们存储在仓库(CubbyHole)的对象中并打印出这些数来;消费者从仓库中取出这些整数并将其也打印出来。同时要求生产者产生一个数字,消费者取得一个数字,这就涉及到两个线程的同步问题。
这个问题就可以通过两个线程实现生产者和消费者,它们共享CubbyHole一个对象。如果不加控制就得不到预期的结果。首先我们设计用于存储数据的类,该类的定义如下:
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 …
为了避免上述情况发生,就必须使生产者线程向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对象;当消费者取得值时,生产者也不能修改它。