您的位置: 网站首页 > 程序开发 > Java程序设计 > 第9章 多线程 > 【9.5 线程的同步与通信】

9.5 线程的同步与通信

 

9.5  线程的同步与通信

9.5.1  线程同步

有时,多个线程需要共享一个对象。如果多个线程同时访问同一个对象,必然会产生对于共享数据的访问冲突。例如一个线程在更新该对象的同时,另一个线程也试图更新或读取该对象,这样将破坏数据的一致性。为避免多个线程同时访问一个共享对象,这里引入同步的概念。

在某个时刻只允许一个线程独占性访问该共享对象,而其他线程只能处于阻塞状态;只有该当线程访问操作结束后,才允许其他线程访问。这称为相互排斥或线程同步。

Java使用synchronized关键字控制对共享信息的并发访问,实现线程同步。Synchron-

ized( )方法相当于一条封装了该方法的整个方法体的同步语句,例如:

Synchronized void method(){

//对共享对象的操作

}

在程序设计中,如果只想实现对某一段代码的同步,可以使用同步代码的方式。

void methodB() {

    //Object obj=new Object();

    synchronized(obj){

        //对共享对象的操作

    }

}

这种同步方式使程序运行速度和可读性都相对较差,但为处理线程同步提供了较灵活的方式。实际上对方法的synchronized 也是通过锁定对象实现的,相当于:

void methodA(){

    synchronized(this){ //对共享对象的操作}

}

【例9-11synchronized 关键字实现代码段同步的示例。

public class BookShelf

{

    String bookName;

    int amount;

    public BookShelf (String name, int amt)

    {

        bookName = name;

        amount = amt;

    }

    public synchronized void putIn(int amt)

    {

        amount += amt;

    }

    public synchronized void withdraw(int amt)

    {

        amount -= amt;

    }

    public int checkRemainder ()

    {

        return amount;

    }

}

Java使用监控器(monitor)来实现对共享数据操作的同步。共享对象都有一个监控器(对象锁)。监控器一次只允许一个线程执行对象的同步块。通过在程序进入同步块时就将对象锁住(获得锁),只有当同步块完成时,监控器才会打开该对象的锁(释放锁),让其他有最高优先级的阻塞线程处理它的同步块。如果一个程序内有两个或以上的方法使用synchronized 关键字,则它们在同一个“对象互斥锁”管理之下。

9.5.2  线程通信

1wait( )notify( )

多线程间除了需要解决对共享数据操作的同步问题,还需要进行线程通信来协调线程间的运行进度问题。Java提供了java.lang.Object类的wait ( )notify( )/notifyAll( )方法来协调线程间的运行进度(读取)关系。

线程已获得某个对象的锁,若该线程调用wait( )方法,则退出所占用的处理器,打开该对象的锁,转为阻塞状态,并允许其他同步语句获得对象锁。当执行条件满足后,将调notify( )方法,唤醒这个处于阻塞的线程,线程转为可运行状态,又有机会获得该对象的锁

如果一个线程通过调用wait( )方法进入对该共享对象的等待状态,应确保有一个独立的线程最终将会调用notify( )方法,以使等待共享对象的线程回到可运行状态。

【例9-12wait( )notify( )方法的例子。

public class WaitTest {

    public static void main(String [] args) {

        ThreadB b = new ThreadB();

        b.start();

        System.out.println("Total b is: " + b.getTotal());

    }

}

class ThreadB extends Thread {

    int total;

    public void run() {

        synchronized(this) {

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

        total += i;

    }

        System.out.println("In ThreadB total is: " + total);

        notify();

        }

    }

        synchronized public int getTotal() {

            try{

            wait();

        }catch(InterruptedException e) {}

        return total;

    }

}

运行结果如下:

In ThreadB total is:

49995000

Total b is:49995000

2.对共享队列数据的读写

9-13是一个线程通信的实例,它说明了如何使用wait( )notify( )方法来协调线程对共享数据的读写访问。

【例9-13在本例中,用synchronized来同步对共享数据的读写,读写代码依赖于一个同步对象,需在其上执行wait( )notify( )方法。如果线程在等待这个同步对象,就需要一个独立的类Queue表示共享对象,并在其中使用notify( )方法。

public class Queue {//共享数据结构——队列

    protected Object[] data;

    protected int writeIndex;

    protected int readIndex;

    protected int count;

    public Queue(int size) {

    data = new Object[size];

}

public synchronized void write(Object value) {//同步对共享数据的写操作

    while(count >= data.length) {

    try{

        wait(); //阻塞,等待共享数据的同步读操作唤醒

        }catch(InterruptedException e) {}

    }

       data[writeIndex++] = value;

       System.out.println("write data is: " + value);

       writeIndex %= data.length;

       count += 1;

       notify(); //唤醒处于阻塞状态的同步读操作

    }

    public synchronized void read() {//同步对共享数据的读操作

        while(count <= 0){

           try{

             wait(); //阻塞,等待共享数据的同步写操作唤醒

           }catch(InterruptedException e) {}

    }

            Object value = data[readIndex++];

            System.out.println("read data is: " + value);

            readIndex %= data.length;

            count =1;

            notify(); //唤醒处于阻塞状态的同步读操作

    }

public static void main(String[] args) {

            Queue q = new Queue(5);

            new Writer(q); //实例化并启动写线程

            new Reader(q); //实例化并启动读线程

    }

}

class Writer implements Runnable{//写线程

    Queue queue;

    Writer(Queue target){

        queue = target;

        new Thread(this).start();

    }

    public void run(){//线程体

        int i = 0;

        while(i<5){

            queue.write(new Integer(i));

            i++;

            }

        }

}

class Reader implements Runnable{//读线程

    Queue queue;

    Reader(Queue source){

        queue = source;

        new Thread(this).start();

}

public void run(){//线程体

    int i=0;

    while(i<5){

      queue.read();

       }

    }

}

运行结果如下:

the end of main()

write data is: 0

write data is: 1

write data is: 2

write data is: 3

write data is: 4

read data is: 0

read data is: 1

read data is: 2

read data is: 3

read data is: 4

因此,有必要设计具有良好行为的线程,并使用wait()notify()进行通信。注意:run()方法保证了在执行暂停或终止之前,共享数据处于一致的状态,这是非常重要的。不应这样来设计程序:随意地创建和处理线程,或创建无数个对话框线程和socket 端点。因为每个线程都会消耗一定的系统资源。

9.5.3  死锁

线程间因为相互等待对方的资源而不能继续执行的情况称为死锁。Java 线程的同步非常容易导致死锁现象,如例9-14所示。

【例9-14程序示例。

Public class DeadlockRisk{

Private static class Resource{

Public int value;

}

private Resource resourceA-new Resource();

private Resource resourceB-new Resource();

public int read(){

synchronized(resourceA) {//这里出现死锁

sysnchronized(resourceB){

return resourceB.value+resourceA.value;}

}

}

public void write(int a, int b){

synchronized(resourceB){//这里出现死锁

synchronized(resourceA){

resourceA.value-a;

resourceB.value-b;}

}

}

}

持有一个锁并试图获取另一个锁时,就有死锁的危险。死锁是由资源的无序使用而带来的,应该在设计程序时尽量避免出现造成死锁的条件。

解决死锁问题的方案是:给资源排序。将所有的线程排列次序并始终遵照这个次序获取锁。例如,如果有3个资源ABC,并有一个线程要获得其中任何一个资源。线程1和线程2都必须确保它在获取B 的锁之前先获得A的锁,以此类推。释放锁时,按照与获取相反的次序。

协调两个需要存取公共数据的线程可能会变得非常复杂。需保证可能有另一个线程存取数据时,共享数据的状态是一致的。因为线程不能在其他线程等待这把锁的时候释放合适的锁,所以必须保证所编写的程序不发生死锁。