操作系统编程实践报告
1 线程的创建与启动
1. 调度的基本单位:线程是程序执行的最小单位,进程是资源分配的最小单位
2. 并发性:进程之间;同一进程的多线程之间
3. 拥有资源:进程可以拥有资源,线程不拥有资源
4. 系统开销:线程只保存少量寄存器,不涉及存储器管理
1.2 Java中的Thread和Runnable类
Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例。启动线程的唯一方法就是通过Thread类的start()实例方法。start()方法是一个native方法,它将启动一个新线程,并执行run()方法。这种方式实现多线程很简单,创建名为MyR的类,作为Runnable接口的实现类并复写run()方法,用while循环控制输出,就可以启动新线程并执行自己定义的run()方法
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();
}
}
线程同步的概念:当使用多个线程来访问同一个数据时,非常容易出现线程安全问题(比如多个线程都在操作同一数据导致数据不一致),所以我们用同步机制来解决这些问题。
为了加深理解,下面举几个例子。
有两个采购员,他们的工作内容是相同的,都是遵循如下的步骤:
(1)到市场上去,寻找并购买有潜力的样品。
(2)回到公司,写报告。
这两个人的工作内容虽然一样,他们都需要购买样品,他们可能买到同样种类的样品,但是他们绝对不会购买到同一件样品,他们之间没有任何共享资源。所以,他们可以各自进行自己的工作,互不干扰。
这两个采购员就相当于两个线程;两个采购员遵循相同的工作步骤,相当于这两个线程执行同一段代码。
下面增加一个角色。一个办公室行政人员这个时候,也走到了布告栏前面,准备修改布告栏上的信息。
如果行政人员先到达布告栏,并且正在修改布告栏的内容。两个采购员这个时候,恰好也到了。这两个采购员就必须等待行政人员完成修改之后,才能观看修改后的信息。如果行政人员到达的时候,两个采购员已经在观看布告栏了。那么行政人员需要等待两个采购员把当前信息记录下来之后,才能够写上新的信息。
上述这两种情况,行政人员和采购员对布告栏的访问就需要进行同步。因为其中一个线程(行政人员)修改了共享资源(布告栏)。而且我们可以看到,行政人员的工作流程和采购员的工作流程(执行代码)完全不同,但是由于他们访问了同一份可变共享资源(布告栏),所以他们之间需要同步
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 块。
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的结果
}
}
l 生产者-消费者(Producer-Consumer)问题是著名的进程同步问题。它描述一组生产者向一组消费者提供消息,它们共享一个有界缓冲池,生产者向其中投放消息,消费者从中取得消息。以下用信号量解决生产者-消费者问题。
定义四个信号量:
empty——表示缓冲区是否为空,初值为n。
full——表示缓冲区中是否为满,初值为0。
mutex1——生产者之间的互斥信号量,初值为1。
mutex2——消费者之间的互斥信号量,初值为1。
设缓冲区的编号为1~n-1,定义两个指针in和out,分别是生产者进程和消费者进程使用的指针,指向下一个可用的缓冲区。
生产者进程
while(TRUE){
生产一个产品;
P(empty);
P(mutex1);
产品送往buffer(in);
in=(in+1)mod n;
V(mutex1);
V(full);
}
消费者进程
while(TRUE){
P(full);
P(mutex2);
从buffer(out)中取出产品;
out=(out+1)mod n;
V(mutex2);
V(empty);
公共电话厅里有多个电话,如某人要打电话,首先要进行申请,看是否有电话空闲,若有,则可以使用电话,如果电话亭里所有电话都有人正在使用,那后来的人只有排队等候。当某人用完电话后,则有空电话腾出,正在排队的第一个人就可以使用电话。这就相当于PV操作:
某人要打电话,首先要进行申请,相当于执行一次P操作,申请一个可用资源(电话);
某人用完电话,则有空电话腾出,相当于执行一次V操作,释放一个可用资源(电话)。
3.3 Java实现该问题的代码
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 测试
当生产能力超出消费能力时,生产者进程等待,可以调大生产者的sleep(),调小消费者的sleep(),从而加快消费者进程,将生产者进程唤醒。
当生产能力弱于消费能力时,消费者进程等待,可以调小生产者的sleep(),调大消费者的sleep(),从而加快生产者进程,将消费者进程唤醒。
通过本次实验,我对进程的调度有了更深层次的了解掌握,明白了进程的创建、控制、通信机制,掌握了线程和进程的区别,对生产者消费者问题有了更深层次的理解,学会了用synchronized解决线程同步问题,收获颇丰。虽然在实验过程中,我遇到很多困难,但是通过解决这些困难,我学到了很多。