我想教你实现Java进程同步,因为我是浪漫的程序员!

2021-12-17 16:15:22 浏览数 (3)

1 问题描述

1.1 为什么要有进程同步

在我们使用的操作系统中为什么要有进程同步机制?我们的计算机系统刚开始是单道批处理系统,意思就是同一时间段内只能运行一个程序,这个程序运行完,才能运行另一个程序,这样就会导致运行效率太低,系统中的资源得不到充分的利用。这也是传统操作系统在进行业务处理的时候效率低下的主要原因,那么对于这种情况应该如何解决呢?这也是现在多道批处理系统出现的原因。

多道程序并发执行,这样大大提高了系统资源的利用率。但是这种系统就会产生一些问题,比如有的资源,比如显示器,cpu,同一时间肯定只能一个程序使用,多个程序肯定不能同时使用显示器,这就是互斥关系,另外,有的两个进程间存在这样的制约关系:A程序的输出是B程序的输入,这是同步关系,为了解决这些问题,我们引入了进程同步机制。通过实现进程同步机制,我们可以实现多个进程对于资源的同步访问,提高资源的使用效率。

1.2 进程同步的方法

对于实现进程同步,我们常用的方法就是实现信号量机制。常用的信号量机制有三种,分别是整型信号量,结构体型信号量和AND型信号量。那么这三种信号量机制具体指什么呢?接下来我对这三种信号量机制逐一进行介绍。

1.2.1 整型信号量

整型信号量是指用一个整数S来进行管理,这个整数S代表资源的数目,我们往往对资源的操作方式有两种,一种是使用,一种是释放。他们分别对应两个函数:wait和signal

wait函数的作用是首先会对资源S进行判断,看资源S可不可以用,如果不可以用,也就是S<=0,那么该进程会一直等下去,直到S成为一个正数,才会执行wait方法。然后S自减一次;而signal的意思则是使用完资源后释放资源,S自增一次。

Wait函数和signal函数的具体实现如下:

代码语言:javascript复制
wait(S){
    while(S<=0);
    S--;
}

signal(S){
    S  ;
}

整型信号量的缺点: 从上面的程序中,我们其实可以发现一个问题,就是当信号量S是小于0的时候,在wait函数中,程序会一直的死循环下去。这样进程就会处于一个“忙等”的状态。这也是使用整型信号量机制的不足之处,那么如果要解决“忙等”,就要让程序进行“让权等待”。也就是在进行无法使用资源的时候,释放处理机,避免长时间占用。

1.2.2 结构体型信号量

对于上面出现的问题,我们应该让哪个进程访问临界资源呢?这个时候结构体信号量就出现了,我们可以使用一个链表来解决这个问题。使用一个结构体来描述资源,这个结构体长这样:

代码语言:javascript复制
typedef struct{
    int value;
    struct process_control_block *list;
}semaphore;

在这个结构体里边有两个变量,一个是value,用来记录资源的个数,第二个是指针,指向下一个要使用临界资源的进程。

通过使用结构体型信号量,wait和signal函数就进行了一些改进,如下这样:

代码语言:javascript复制
wait(semaphore *S){
    S->value--;
    if(S->value<0)  block(S->list);
}

signal(semaphore *S){
    s->value  ;
    if(S->value<=0)
      wakeup(S->list);
}

在程序中,S->value的初值表示系统中某类资源的数目,可称为资源信号量。

注: S>0时:表示可供并发进程使用的资源数。 S<=0时:|S|表示因缺少该资源而自我阻塞的进程数。

在改进之后的wait函数中,首先会向操作系统申请资源,并且让资源数减一。如果资源分配完毕,那么就调用block原语将进程进行自我阻塞。

而在signal函数中,首先会释放资源,让资源数减一,之后进行判断,如果该信号量链表中仍有等待的进程被阻塞,那么就调用wakeup原语将其唤醒。

1.2.3 AND型信号量

AND型信号量是将进程在整个运行过程中的所有资源,一次性全部分配给进程,进程使用完后再一次性释放,只要一个进程尚未分配成功,其他所有为之分配的资源也不分配给它。也就说要么把它所请求的资源全部分配给进程,要么一个也不分配,这样的好处是可以很好的避免死锁现象。

使用该信号量机制时,wait和signal函数的实现如下:

代码语言:javascript复制
wait(S1,S2,...,Sn)
{
    while(true)
    {
        if(Si>=1&&...&&Sn>=1)
        {
            for(i=1;i<n;i  )
            Si--;
            break;
        }
        else
        {
            place the process in the waiting queue associated with.....
            //如果没有申请到资源的话就挂到相应的阻塞队列里去
        }
    }
}

Ssignal(S1,S2,...Sn)
{
    while(true)
    {
        for(i=1;i<=n;i  )
        {
            Si  ;
            remove all the process waiting in the queue associated with Si into the ready queue;
            //去后备队列里唤醒或移除因Si资源阻塞的进程
        }
    }
}

以上就是对三种信号量机制的作用和实现原理的说明。

1.3 使用信号量机制解决问题

使用信号量机制所要解决的问题还是进程的互斥和同步问题。那么这两种问题又是如何实现的呢?

1.3.1 实现互斥关系

假设两个进程PA,PB具有互斥关系,也就是他们要使用通同一个临界资源,对于这种情况应该怎么做呢?我们可以设置一个mutex信号量,初值设为1,这样在最开始的时候,两个进程都能使用该资源,在进程PA使用资源的时候,首先会调用wait函数让资源数减一,wait函数完成之后会让信号量mutex-1,这样mutex的值此时为0,另一个进程PB就不能使用该资源了,在进程PA使用完资源以后会调用signal函数释放资源,让信号量mutex 1,mutex此时会重新变成1,另一个进程PB就可以使用该临界资源了。

代码语言:javascript复制
semaphore mutex=1;
PA(){
    while(1){
        wait(mutex);
        临界区
        signal(mutex);
        剩余区
    }
}

PB(){
    while(1){
        wait(mutex);
        临界区
        signal(mutex);
        剩余区
    }
}

1.3.2 实现前驱关系

假设P1和P2有前驱关系,P1执行完,P2才能执行,那么这种应该怎么实现呢?这时可以设置一个公共的信号量S,初值设为0。

进程P1中:S1;signal(S); 进程P2中:wait(S);S2;

上面语句的意思是,先执行P1的语句,然后释放S,也就是S ,这样当P2执行完wait函数之后才可以执行,否则,不执行signal函数的话,S就为0,P2也无法执行,这样就实现了P1和P2的前驱关系。

接下来,我们将通过实验的方式来对上面描述的实现进程同步和互斥的问题进行实验。验证这种方式的实际使用性。

2 程序设计

2.1 需求分析

生活中我们经常遇到的生产者/消费者问题其实就是一个典型的进程同步问题,其中的生产者产生资源,消费者消耗资源。比如典型的买包子问题,我们可以通过它来模拟进程的同步。消费者与生产者进程之间的执行都依赖于另一个进程的消息,想要表现同步机制,这需要使用Java中的wait() / notify()方法实现同步机制。由于包子余量(资源数量)需要所有进程共享,因此任意时刻只能有一个进程访问缓冲器,这需要使用Java中的synchronized同步代码块实现,synchronized关键字的作用就是控制多个线程访问资源同步性的问题,它的三种使用方式是:修饰实例方法、修饰静态方法、修饰代码块。

如果方法或代码块用 synchronized 进行声明,那么对象的锁将保护整个方法或代码块,要调用这个方法或者执行这个代码块,必须获得这个对象的锁。而且,任何时候都只能有一个线程对象执行被保护的代码。

2.2 算法设计思路

我们以买包子问题为例,现设计2个厨师(生产者),2个顾客(消费者),包子铺包子存储上限为3(缓冲器大小)。包子初始值为0,此时所有买家进程会进入等待状态,所有的厨师进程会在包子余量不超过缓冲器大小前不停做包子,并唤醒买家进程已经有包子可吃了,直至缓冲器满了进入等待状态,而买家进程每吃掉一个包子后都会唤醒厨师进程可以继续做包子了。同时由于包子余量需要所有进程共享,保证任意时刻只能有一个进程访问缓冲器,因此所有进程方法都需要用synchronized声明。

3 源代码清单

3.1 缓冲器公共类

代码语言:javascript复制
package com.common;

/**
 * 定义缓冲器余量,生产者和消费者执行的方法
 */
public class BaoZi {
    Integer count = 0;      //记录包子总数

    /**
     * 生产包子(产生资源)
     *
     * @param name 厨师名
     */
    public synchronized void makeBaoZi(String name) {
//        判断包子数是否大于3达到上限
        while (count >= 3) {
            System.out.println(name   ":包子已经达到上限了!");
            try {
                wait();     //包子容量达到3,超出上限,生产者进入等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count  ;    //如果没有达到3就做一个包子
        System.out.println(name   ":做了一个包子,还剩【"   count   "】个!");
        notifyAll();    //有包子资源了,唤醒消费者购买包子
    }

    /**
     * 购买包子(使用资源)
     *
     * @param name
     */
    public synchronized void buyBaoZi(String name) {
//        判断包子资源数是否等于0,
        while (count == 0) {
            System.out.println(name   ":没有包子资源了!");
            try {
                wait();     //没有包子资源,消费者进程进入等待状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;    //包子资源数量减一
        System.out.println(name   ":购买了一个包子,包子资源剩余【"   count   "】个!");
        notifyAll();    //包子数未达到上限,让生产者厨师做包子,(唤醒所有等待该资源的进程),
    }
}

3.2 生产者进程类

代码语言:javascript复制
package com.producer;

import com.common.BaoZi;

/**
 * 生产者进程
 */
public class BaoZiPu extends Thread {
    private String name;    //厨师名字
    private BaoZi baoZi;    //缓冲区包子资源

    public BaoZiPu() {
    }

    public BaoZiPu(BaoZi baoZi, String name) {
        this.baoZi = baoZi;
        this.name = name;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(2000);     //休眠两秒钟
                baoZi.makeBaoZi(name);  //生产一个包子资源
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

3.3 消费者进程类

代码语言:javascript复制
package com.consumer;

import com.common.BaoZi;

/**
 * 消费者进程,消耗包子资源
 */
public class Customer extends Thread {

    private String name;
    private BaoZi baoZi;

    public Customer() {
    }

    public Customer(BaoZi baoZi, String name) {
        this.baoZi = baoZi;
        this.name = name;
    }

    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(2000); //休眠两秒钟
                baoZi.buyBaoZi(name);   //消耗一个包子资源
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

3.4 测试类

代码语言:javascript复制
import com.common.BaoZi;
import com.consumer.Customer;
import com.producer.BaoZiPu;

public class TestMain {

    public static void main(String[] args) {
        System.out.println("==========进程同步测试==========");
        BaoZi baoZi = new BaoZi();
        Thread rouBaoZi = new Thread(new BaoZiPu(baoZi, "肉包子厨师"));
        Thread suBaoZi = new Thread(new BaoZiPu(baoZi, "素包子厨师"));
        Thread customerZS = new Thread(new Customer(baoZi, "顾客张三"));
        Thread customerLS = new Thread(new Customer(baoZi, "顾客李四"));
        customerZS.start();
        rouBaoZi.start();
        suBaoZi.start();
        customerLS.start();

    }
}

4 运行结果测试与分析

4.1 测试运行结果

4.2 实验结果分析

根据实验测试结果我们可以做出这样的分析,在我们创建好四个进程时,让四个进程共享一个缓存区资源(包子),之后让顾客张三先去购买包子,结果包子此时是0个,那么该进程进入等待状态,之后素包子厨师和肉包子厨师访问资源开始创建包子,在创建好一个包子资源之后,此时顾客李四也开始去访问包子资源。所有就购买了一个包子。包子数剩余0个,这个时候肉包子厨师的包子做好了,所以现在会有一个包子资源,但是会被等待队列中的张三买走。此时包子资源的数量是没有达到上限的,那么包子厨师就会不断的创建包子资源,同时顾客也会不断的去买包子,直到包子数量为0时,顾客再去购买就会提醒没有包子了。

5 结论

设计这个实验的目的就是为了验证和测试操作系统下进程同步的问题,通过实验学习和代码实践,让我对进程间同步和互斥机制有了更加深刻的认识和理解。在这里我通过包子铺卖包子和买家买包子的案例模拟生产者/消费者问题实现进程间的同步和互斥。

对于生产者和消费者对缓冲区的访问,都是有两个限定条件的。首先对于包子厨师能不能生产包子放到缓冲器中,需要两个条件是:第一,缓冲器需要空闲,即包子余量有没有达到上限;第二,获取当前资源对象的锁,判断有没有其他生产者或消费者在缓冲器中。对于顾客购买包子访问缓冲器也需要两个条件:第一是缓存器中存在资源,也就是有包子,第二是判断有没有其他生产者或消费者在缓冲器中,这都是需要synchronized关键字同步代码块来实现的。

0 人点赞