操作系统编程实践课程设计

操作系统编程实践课程设计

1 线程的创建与启动

1.1 进程与线程

1. 概念

进程:是指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。

用户每启动一个进程,操作系统就会为该进程分配一个独立的内存空间。

线程:线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。线程有就绪、阻塞和运行三种基本状态。

一个程序至少一个进程,一个进程至少一个线程。

2.差别

1进程是资源的分配和调度的一个独立单元,而线程是系统调度的基本单元。

2)同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。

3同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程

4每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。

1.2 Java中的Thread和Runnable类

在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限,

Thread类

Thread类实现了Runnable接口,在Thread类中,有一些比较关键的属性,比如name是表示Thread的名字,可以通过Thread类的构造器中的参数来指定线程名字,priority表示线程的优先级(最大值为10,最小值为1,默认值为5),daemon表示线程是否是守护线程,target表示要执行的任务。

下面是Thread类中常用的方法

1)start方

start()用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

2)run方法

run()方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

3)sleep方法

sleep方法有两个重载版本:

sleep(long millis)  //参数为毫秒 

sleep(long millis,int nanoseconds)  //第一参数为毫秒,第二个参数为纳秒

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。

4)yield方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。

5)join方法

join方法有三个重载版本:

join()

join(long millis)     //参数为毫秒

join(long millis,int nanoseconds)    //第一参数为毫秒,第二个参数为纳秒

假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。

6)interrupt方法

interrupt,顾名思义,即中断的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。

Runnable类

Runnable接口非常简单,就定义了一个方法run(),继承Runnable并实现这个方法就可以实现多线程了,但是这个run()方法不能自己调用,必须由系统来调用,否则就和别的方法没有什么区别了.

1.3 三种创建线程的办法

1. 继承Thread类创建线程类

(1)创建一个Runnable接口的对象并使用Thread对象启动它。定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。(2)创建Thread子类的实例,即创建了线程对象。(3)调用线程对象的start()方法来启动该线程。

package os;

/**

 * Runnable的实现类,是线程执行的主体

 * run函数是入口

 *

 */

class MyR implements Runnable{

private String msg;

public MyR(String msg) {

this.msg=msg;

}

//线程的入口

@Override

public void run() {

while(true) {

try {

System.out.println(msg);

Thread.sleep(10000);

} catch (InterruptedException e) {

e.printStackTrace();

break;

}

}

}

}

public class testthread {

 

public static void main(String[] args) {

// 创建线程

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

thread1.start();//启动了线程

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

thread2.start();

}

}

 

2. 通过Runnable接口创建线程类

(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。(3)调用线程对象的start()方法来启动该线程。 

package os;

 

public class Testthread2 {

 

public static void main(String[] args) {

Testthread2 testThread2 = new Testthread2();

// 匿名信 匿名类 引用就是指针

Runnable runnable = new Runnable() {

 

@Override

public void run() {

while (true) {

try {

System.out.println("haha");

Thread.sleep(10000);

 

} catch (InterruptedException e) {

// TODO Auto-generated catch block

e.printStackTrace();

break;

}

}

}

};

Thread thread = new Thread(runnable);

thread.start();

}

}

3.通过Callable和Future创建线程

(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

package os;

 

public class TestThread3 {

 

public static void main(String[] args) {

// TODO Auto-generated method stub

new Thread(new Runnable() {

 

@Override

public void run() {

System.out.println("haha");

}

}).start();

// lamda 表达式 java 1.8+

new Thread(() -> {

System.out.println("haha");

}).start();

 

}

}

 

 

 

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

2.1 同步的概念和必要性

1.同步的概念

线程同步就是同步协调,按预定的先后次序进行运行线程同步是指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的

2. 同步的必要性

线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。

 

为了加深理解,下面举几个例子。

有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:

1)到市场上去,寻找并购买有潜力的样品。(2)回到公司,写报告。

这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。下面给这两个采购员增加一个工作步骤。采购员需要根据公司的“布告栏”上面公布的信息,安排自己的工作计划。这两个采购员有可能同时走到布告栏的前面,同时观看布告栏上的信息。这一点问题都没有。因为布告栏是只读的,这两个采购员谁都不会去修改布告栏上写的信息。

 

下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。

上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步。

2.2 synchronize关键字和同步块

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

 

2.3 实例

Java中如何通过同步块实现同步

package os;

public class test {

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; //

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

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

System.out.println("thread"+index+"enter");

}

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

a++;//将值加一

try {

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

}catch (InterruptedException e) {

// TODO: handle exception

e.printStackTrace();

c=a;

System.out.println("thread"+index+"leave");

}

});

threads[i].start();

}

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

try {

threads[i].join();

}catch(InterruptedException e) {

e.printStackTrace();

}

}

System.out.print("c="+c);

}

}

 

 

3 生产者消费者问题

3.1 问题表述

该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据

3.2 实现思路

要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题,常用的方法有信号灯法等。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

3.2 Java实现该问题的代码

package os;

 

import java.util.concurrent.locks.Condition;

import java.util.concurrent.locks.Lock;

 

/**

 * 生产者

 */

public class Producer implements Runnable {

private Queue q;

private Condition isFull; // 信号量 , 如果满了则等待

private Condition isEmpty; // 信号量 , 如果空了则等待

private Lock lock;

private int index; // 生产者的编号

 

public Producer(int index, Queue q, Lock lock, Condition isFull, Condition isEmpty) {

this.index = index;

this.q = q;

this.isFull = isFull;

this.isEmpty = isEmpty;

this.lock = lock;

}

 

@Override

public void run() {

lock.lock();

if (q.isFull()) {

try {

isFull.await(); // 如果队列为,则等待

} catch (InterruptedException e) {

return;

}

}

// 生产并入队

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

q.EnQueue(a);

// 生产完后

isEmpty.signalAll();// 把消费者唤醒。

lock.unlock();

}

 

}

 

3.4 测试

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

当生产能力超出消费能力时,生产者线程生产物品时没有空缓冲区可用,生产者线程必须等待消费者线程释放出一个空缓冲区。   

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

生产能力弱于消费能力时,消费者线程消费物品,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产者线程生产出来。

4 总结

在生产者-消费者问题中,生产者消费者都以缓冲区作为资源,并且都有两个状态,空状态是消费者的资源,满状态是消费者的资源。P进程不能往满的缓冲区放产品,设置信号量empty,初值为0,用于指示空缓冲区数目。 Q进程不能从空的缓冲区取产品,设置信号量full,初值为0,用于知识满缓冲区数目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值