Java同步机制

 

 

Java同步机制

为解决共享数据的访问问题,java的同步机制包括两个方面

1.共享数据的线程互斥锁定

2.传送数据的线程同步运行

 

第一节 互斥

多个线程可能会访问相同的资源,此时不能保证程序运行结果的正确性。解决办法是“互斥”。

以银行账户存取作为模型来讲述互斥的意义。参见《Java程序设计》宋中山严山钧的P277。

账户Account的存款、取款操作分别设计为线程类Save、Fetch。

 

class Account //账户类

{

    privateintvalue;

    void put(int i){value=value+i;}

    int get(int i){

       if (value>=i){

           value=value-i;

           return i;

       }

       elsereturn 0;

       }

    int howmatch(){returnvalue;}

}

class Save extends Thread   //存款线程类

{

    private Account a;

    privateintamount;

    public Save(Account a,int amount){this.a=a;this.amount= amount;}

    publicvoid run()

    {

       System.out.println("现有"+a.howmatch());

              //此处可接着调用sleep()方法以更方便演示

       a.put(amount);

       System.out.println("存入"+ amount);

       System.out.println("余额"+a.howmatch());

    }

}

 

class Fetch extends Thread   //取款线程类

{

    private Account a;

    privateintamount;

    public Fetch (Account a,int amount){this.a=a;this.amount= amount;}

    publicvoid run()

    {

       System.out.println("现有"+a.howmatch());

              //此处可接着调用sleep()方法以更方便演示

       System.out.println("取出"+a.get(amount));

       System.out.println("余额"+a.howmatch());

    }

}

 

class Test{              //主程序

    publicstaticvoid main(String []args){

       Account a = new Account();

       Save s1= new Save(a,100);s1.start();

       Save s2= new Save(a,200);s2.start();

       Fetch f1=new Fetch(a,250);f1.start();

       }

}

 

在本例中,main()线程创造了3个线程对象(2Save1Fetch),它们共享账户a。由于这3个线程的run()方法在同时执行时都能够操作账户a3个线程的run()方法中的语句执行进度是不确定的,导致每次运行结果都不一样,而且运行结果可能与任何串行运行结果都不相同,所以上述代码不完善。

比如,一个运行结果输出如下表,经分析,这个输出与任何串行运行结果都不相同,不符情理。

现有0

存入200

余额200

现有0

存入100

余额300

现有300

取出250

余额50

解决的办法是,用synchronized关键字在临界区对共享的资源(这里是“账户”)进行加锁。本例即是对run()方法块进行加锁。修改后的Save类、Fetch类如下:

 

class Save extends Thread   //存款线程类

{

    private Account a;

    privateintamount;

    public Save(Account a,int amount){this.a=a;this.amount= amount;}

    publicvoid run()

    {

       synchronized(a){

        System.out.println("现有"+a.howmatch());

              //此处可接着调用sleep()方法以更方便演示

       a.put(amount);

       System.out.println("存入"+ amount);

       System.out.println("余额"+a.howmatch());

       }

    }

}

 

class Fetch extends Thread   //取款线程类

{

    private Account a;

    privateintamount;

    public Fetch (Account a,int amount){this.a=a;this.amount= amount;}

    publicvoid run()

    {

       synchronized(a){

       System.out.println("现有"+a.howmatch());

              //此处可接着调用sleep()方法以更方便演示

       System.out.println("取出"+a.get(amount));

       System.out.println("余额"+a.howmatch());

       }

    }

}

 

这样,对于某一个账户a,某时刻最多只会有一个线程能够进入到被synchronized(a)标示的代码块中。运行结果虽然也有很多种,3个线程进入临界区的先后是不确定的。但运行结果均与某种串行运行结果相同。

 

思考:

另外,需要注意的是,可能你会考虑能否仅对Account类的put()get(方法施加synchronized,而不对SaveFetch类的run()的代码块施加synchronized,即

class Account //账户类

{

    privateintvalue;

 

    synchronized void put(int i){value = value +i;}

    synchronized int get(int i){

       if (value >=i){

           value = value -i;

           return i;

       }

       elsereturn 0;

       }

    int howmatch(){returnvalue;}

}

 

但是,这样就仅能保证账户中的最终金额不会错乱,但SaveFetch类中的run()方法中的语句的执行进度仍然是随机的,运行结果不符合串行化要求。

 

第二节 同步

多个线程在访问相同的资源时,我们可能要求线程以及线程之间必须满足一定的条件。表面上,这些条件导致了线程访问过程具有“步调”,即一切均在严密的控制当中,这种控制实际上在程序设计中被称为“同步”。

 

以生产者-消费者问题作为模型来讲述同步的意义。参见《Java程序设计》宋中山严山钧的P284。

 

生产者Producer,消费者Consumer,仓库Cubbyhole。生产者向仓库放入产品,消费者从仓库拿出产品。

生产者、消费者代表线程,仓库代表资源。

 

条件constraint:1.当仓库满时,生产者将不能继续向仓库放入产品;2.当仓库空时,消费者将不能继续从仓库拿出产品。

 

为简单起见,假设仓库的容量为1,即只能容纳1个产品。并用向仓库赋一个值模拟放入产品,用从仓库读出该值模拟拿出产品。

 

假设我们不考虑上述条件,则设计出的程序代码如下:

 

//生产者

class Producer extends Thread{

    private CubbyHole cubbyhole;

    privateintnumber;         //生产者编号

    public Producer(CubbyHole cubbyhole, int number) {

       this.cubbyhole = cubbyhole;

       this.number = number;

    }

 

    publicvoid 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){}

       }

    }

}

 

//消费者

class Consumer extends Thread{

    private CubbyHole cubbyhole;

    privateintnumber;         //消费者编号

    public Consumer(CubbyHole cubbyhole, int number) {

       this.cubbyhole = cubbyhole;

       this.number = number;

    }

    publicvoid run() {

       int value=0;

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

           value=cubbyhole.get();

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

       }

    }

}

 

//仓库

class CubbyHole{

    privateintseq;

    publicsynchronizedvoid put(int value){seq=value;}

    publicsynchronizedint get(){returnseq;}

}

 

 

//主程序

class test{

    publicstaticvoid main(String[] args){

       CubbyHole c=new CubbyHole();

       Producer p1=new Producer(c, 1);

       Consumer c1=new Consumer(c, 1);

       p1.start();

       c1.start();

    }

}

 

 

本例中,生产者不断的向cubbyhole对象写入整数(依次从110),消费者不断的从此cubbyhole对象读出其中的数据。

该代码不能保证生产者每写入一个数据,消费者就及时读出一个数据并且只读一次。该代码的CubbyHole类的put()get()方法用synchronized修饰,这只保证了同一时刻要么put()方法正在运行要么get()方法正在运行,但没有保证put()get()方法交替运行,即不能控制“步调”。如果生产者比消费者快,那么消费者来不及取一个数据前,生产者就又产生了新的数据;如果消费者比生产者快,消费者可能两次取同一个数据。

比如,一个运行结果输出如下表,经分析,这个输出不满足条件constraint,是消费者比生产者快的情况。

 

Producer#1put:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Consumer#1get:0

Producer#1put:1

Producer#1put:2

Producer#1put:3

Producer#1put:4

Producer#1put:5

Producer#1put:6

Producer#1put:7

Producer#1put:8

Producer#1put:9

 

为了能满足条件constraint(1.当仓库满时,生产者将不能继续向仓库放入产品;2.当仓库空时,消费者将不能继续从仓库拿出产品),需要用到一个所谓的“监视器”,即对条件的形式化、程序语言化的描述,以控制线程运行。

修改CubbyHole类的代码如下:

//仓库

class CubbyHole{

    privateintseq;

    privatebooleanavailable =false//监视器,该例只用一个简单的测试变量

    publicsynchronizedvoid put(int value){

       while(available==true){  //判断条件

           try{

              wait();

           }

           catch(InterruptedException e){}

       }

      

       seq=value;    //做具体处理

       available=true;//修改状态

       notifyAll();  //唤醒等待队列中的线程

       }

    publicsynchronized int get(){

       while(available ==false){   //判断条件

           try{

              wait();

           }

           catch(InterruptedException e){}

       }

       int value=seq;    //做具体处理

       available =false;//修改状态

       notifyAll();  //唤醒等待队列中的线程

       return value;

       }

}

 

说明:

为了实现同步控制,java提供了wait()、notifyAll()、notify()方法。

wait()方法使当前线程变为阻塞状态,线程主动释放了当前线程锁定的任何对象(而sleep()方法不会),进入等到队列。

notifyAll()方法唤醒等待队列中的全部线程,notify()方法唤醒等待队列中的某个线程。

wait()、notifyAll()、notify()方法是Object类的方法,因此所有的类都继承了这些方法。但只能在synchronized代码块(方法)中调用这些方法,否则会抛出IllegalMonitorStateException异常;

 

总结:同步机制的代码设计如下

(1)如果多个线程修改(访问)同一个对象(资源),那么将执行修改(访问)的方法,用synchronized修饰。

(2)如果一个线程必须等到某个状态条件成立时才能继续运行的话,那么此线程应在对象(资源)队列中等待,这要通过进入synchronized方法然后调用wait()方法来做到。直到状态条件成立,才能接着做处理。

(3)每当某一个(线程的)方法对资源做完具体处理并影响、修改了状态后,这个方法就应该接着调用notify()或者notifyAll()方法,(这会使其它线程的wait()方法立即返回),这样给那些等待队列中的线程一个机会,这些线程会检测运行环境是否已满足自己重新运行的需要,如果不满足则继续调用wait()来等待,否则有可能会被调度程序选定而运行。

上述(1)(2)(3)点的形式化描述如下:

    public synchronized void method()

{

       while(状态条件){

           try{

              wait();

           }

           catch(InterruptedException e){}

       }

      

       具体处理;

       修改状态;

       notifyAll()notify();

    }

 

思考:上述例子的仓库(资源)是简化版的,即容量为1,放产品和取产品是用设置一个整数的值来模拟的。这里考虑仓库是一个有着容量上限的资源池(用集合HashSet表示),其中存放了大量对象Object。代码如下:

//仓库

class CubbyHole{

    private HashSet pool;

    //其它属性

 

    publicsynchronizedvoid put(Object o){

       while(pool计数器指示满了){ //判断条件

           try{

              wait();

           }

           catch(InterruptedException e){}

       }

      

       pool中放资源o//做具体处理

       pool计数器加1//修改状态

       notifyAll()notify();   //唤醒等待队列中的线程

       }

 

    publicsynchronized Object get(){

       while(pool计数器指示为空){ //判断条件

           try{

              wait();

           }

           catch(InterruptedException e){}

       }

       pool中取资源o//做具体处理

       pool计数器减1//修改状态

       notifyAll()notify();   //唤醒等待队列中的线程

       return o;

       }

}

 

第三节 死锁

以哲学家吃饭问题(Dijkstra发现)作为模型来讲述死锁的情况。参见《Java程序设计》宋中山严山钧的P286。

Java语言没有提供能够预防、解除死锁的机制,所以这应该由程序员来实现。解决死锁有两种方案:1:预防死锁:即采用技术使程序永远不会进入死锁的状态;2:解除死锁:当程序进入死锁状态时,能检测到然后采用技术解除死锁状态。

 

 

 

法1:一次性锁定所有要用到的资源,而不是先锁定一部分资源,再锁定另一部分资源。

则修改Philosopher类如下

 

 

你可能感兴趣的:(Java线程编程,java,Java,JAVA,java,互斥,同步,线程)