操作系统编程实践

      操作系统编程实践报告

1 线程的创建与启动

1.1 进程与线程

线程与进程的比较:

1.     调度的基本单位:线程是程序执行的最小单位,进程是资源分配的最小单位

2.     并发性:进程之间;同一进程的多线程之间

3.     拥有资源:进程可以拥有资源,线程不拥有资源

4.     系统开销:线程只保存少量寄存器,不涉及存储器管理

1.2 Java中的Thread和Runnable类

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,创建名为MyR的类,作为Runnable接口的实现类并复写run()方法,用while循环控制输出,就可以启动新线程并执行自己定义的run()方法

1.3 三种创建线程的办法

1、继承Thread类创建线程

packagesrc;

classMyR implementsRunnable{

private String msg;

public MyR(String msg){

this.msg=msg;

}

    @Override

    publicvoidrun() {

       while(true){

           try {

              Thread.sleep(1000);

           }catch (InterruptedException e) {

              e.printStackTrace();

              break;

           }

       }

      

    }

   

}

publicclass teseheard {

 

    publicstaticvoid main(String[] args) {

       Threadthread1 = new Thread(new MyR("hello"));

       thread1.start();

       Threadthread2 = new Thread(new MyR("wuwu"));

       thread2.start();

    }

}

2、实现Runnable接口创建线程
package org.yang;

 

public classTestThread2 {

 

    public static void main(String[] args) {

      //创建匿名类,引用就是指针

       Runnable runnable = new Runnable() {//Runnable是一个接口

 

           @Override

           public void run() {

               while(true){

                      try {

                         System.out.println("haha");

                         Thread.sleep(1000);

                     } catch (InterruptedException e) {

                         e.printStackTrace();

                         break;

                     }

                   }    

              }

       };

       Thread thread = new Thread(runnable);

       thread.start();

    }

 

}

3.第三个实验中,用两种方法创建线程

package org.yang;

 

import javax.swing.event.TreeWillExpandListener;

 

public class TestTread3{

 

    public static void main(String[] args) {

       new Thread(new Runnable() {

          

           @Override

           public void run() {

               System.out.println("haha");

              }

       }).start();

      

       //lambda 表达式 java 1.8+

       new Thread(()->{

           System.out.println("haha");

       }).start();

    }

 

}

2 线程简单同步(同步块)

2.1 同步的概念和必要性

线程同步的概念:当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。

为了加深理解,下面举几个例子。 
有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤: 
(1)到市场上去,寻找并购买有潜力的样品。 
(2)回到公司,写报告。 
这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。 
这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。

下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。 
如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。 
上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步

2.2 synchronize关键字和同步块

 synchronized关键字

synchronized用于解决线程同步问题,当有多条线程同时访问共享数据时,如果不进行同步,就很可能会发生错误,java提供的解决方案是:只要将操作共享数据的代码在某一时间让一个线程执行完,在执行过程中,其他线程不能执行同步代码,这样就可以保护数据的正确性。

Synchronize 关键字是解决并发问题常用解决方案,有以下三种使用方式:

·        同步普通方法,锁的是当前对象。

·        同步静态方法,锁的是当前 Class 对象。

·        同步块,锁的是 {} 中的对象

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
(1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
(2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
(3)修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
(4)修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

同步方法与同步代码块

同步方法

1.同步方法 

 即有synchronized关键字修饰的方法。 

 由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

    代码如: 

    public synchronized voidsave(){}

 

   注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

2.同步代码块 

 即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

    代码如: 

    synchronized(object){ 

    }


synchronized
方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void drawMoney()

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

2.3 实例

package org.yang;

 

public class TestSync {

    static int c = 0;

    public static void main(String[] args) {

       Thread[] threads = new Thread[1000];

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

           threads[i] = new Thread(()->{

              int a = c;//获取c的值

              a++;//将值加一

              try {//模拟复杂处理过程

                  Thread.sleep((long)(Math.random()*1000));

              } catch(InterruptedException e) {

                  e.printStackTrace();

              }

              c=a;//存回去

           });

           threads[i].start();//线程开始

       }

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

           try {

              threads[i].join();//等待thread i的完成

           }catch(InterruptedException e) {

              e.printStackTrace();

           }

       }//循环后,所有的线程都完成了

       System.out.print("c="+c);//输出c的结果

    }

 

}

 

一个进程进,等它出来后,下一个进程才能进去,最后c的值为1000。

1、随便建立一个变量,作为锁变量

2、创建一个同步块,需要一个锁

3、建立了一个final变量,方便在lamdba中使用

4、输出

改进代码:

package org.yang;

 

import com.sun.media.jfxmedia.events.NewFrameEvent;

 

public class TestSync {

    static int c = 0;

    static Object lock = new Object();//1、随便建立一个变量,作为锁变量

    public static void main(String[] args) {

       Thread[] threads = new Thread[1000];

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

           final int index = i;//4、建立了一个final变量,方便在lambda中使用

           threads[i] = new Thread(()->{

              synchronized (lock) {//2、创建了一个同步块,需要一个锁

                  System.out.println("thread"+index+"enter");//5、输出

             

              int a = c;//获取c的值

              a++;//将值加一

              try {//模拟复杂处理过程

                  Thread.sleep((long)(Math.random()*10));

              } catch(InterruptedException e) {

                  e.printStackTrace();

              }

              c=a;//存回去

              System.out.println("thread"+index+"leave");//6、输出

              }//3、这是块的终结

           });

           threads[i].start();//线程开始

       }

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

           try {

              threads[i].join();//等待thread i的完成

           }catch(InterruptedException e) {

              e.printStackTrace();

           }

       }//循环后,所有的线程都完成了

       System.out.print("c="+c);//输出c的结果

    }

 

}

3 生产者消费者问题

3.1 问题表述

生产者-消费者(Producer-Consumer)问题是著名的进程同步问题。它描述一组生产者向一组消费者提供消息,它们共享一个有界缓冲池,生产者向其中投放消息,消费者从中取得消息。以下用信号量解决生产者-消费者问题。

3.2 实现思路

定义四个信号量:

empty——表示缓冲区是否为空,初值为n

full——表示缓冲区中是否为满,初值为0

mutex1——生产者之间的互斥信号量,初值为1

mutex2——消费者之间的互斥信号量,初值为1

设缓冲区的编号为1n-1,定义两个指针inout,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。

 

生产者进程

while(TRUE){

     生产一个产品;

     P(empty);

     P(mutex1)

     产品送往bufferin);

     in=(in+1)mod n

     V(mutex1);

     V(full);

     }

消费者进程

while(TRUE){

 P(full);

   P(mutex2)

   bufferout)中取出产品;

   out=(out+1)mod n

   Vmutex2);

   V(empty);

例子:在公共电话厅打电话

公共电话厅里有多个电话,如某人要打电话,首先要进行申请,看是否有电话空闲,若有,则可以使用电话,如果电话亭里所有电话都有人正在使用,那后来的人只有排队等候。当某人用完电话后,则有空电话腾出,正在排队的第一个人就可以使用电话。这就相当于PV操作:

某人要打电话,首先要进行申请,相当于执行一次P操作,申请一个可用资源(电话);

某人用完电话,则有空电话腾出,相当于执行一次V操作,释放一个可用资源(电话)。

3.3 Java实现该问题的代码

package org.yang;

 

import javax.print.attribute.standard.PrinterMessageFromOperator;

 

public class TestPC {

    static Queue queue = new Queue(5);

    public static void main(String[] args) {

       //创建生产者

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

           final int index =i;

           new Thread(()->{

              while(true) {

              int data = (int)(Math.random()*1000);

              System.out.printf("Producer thread%d want to enque %d\n",index,data);

              queue.EnQueue(data);

              System.out.printf("Producer thread%d EnQueue %d Success\n",index,data);

              sleep();//随机休息一段时间

              }

           }).start();

       }

       //创建消费者

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

           final int index =i;

           new Thread(()->{

              while(true) {

              System.out.printf("Consumer thread%d want to Dequeue\n",index);

              int data = queue.DeQueue();

              System.out.printf("Consumer thread%d DeQueue %d Success\n",index,data);

              sleep2();//随机休息一段时间

              }

             

           }).start();

       }

    }//sleep随机时间

    public static void sleep() {

       int t =(int)(Math.random()*100);

       try {

           Thread.sleep(t);

           } catch(InterruptedException e) {

              e.printStackTrace();

           }

    }

    public static void sleep2() {

       int t =(int)(Math.random()*100);

       try {

           Thread.sleep(t);

           } catch(InterruptedException e) {

              e.printStackTrace();

           }

    }

}

3.4 测试

3.4.1 当生产能力超出消费能力时的表现

    当生产能力超出消费能力时,生产者进程等待,可以调大生产者的sleep(),调小消费者的sleep(),从而加快消费者进程,将生产者进程唤醒。

3.4.2 当生产能力弱于消费能力时的表现

当生产能力弱于消费能力时,消费者进程等待,可以调小生产者的sleep(),调大消费者的sleep(),从而加快生产者进程,将消费者进程唤醒。

4 总结

通过本次实验,我对进程的调度有了更深层次的了解掌握,明白了进程的创建、控制、通信机制,掌握了线程和进程的区别,对生产者消费者问题有了更深层次的理解,学会了用synchronized解决线程同步问题,收获颇丰。虽然在实验过程中,我遇到很多困难,但是通过解决这些困难,我学到了很多。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值