操作系统编程实践总结

1线程的创建与启动

1.1 进程与线程

一、什么是进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

二、什么是线程

线程,有时被称为轻量级进程(Lightweight ProcessLWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪阻塞运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程

三、进程和线程的区别

1. 一个程序至少有一个进程,一个进程至少有一个线程

2. 线程的划分尺度小于进程,使得多线程程序的并发性高

3. 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率

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

5. 多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配

1.2 Java中的ThreadRunnable

1、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方法,其他线程也无法访问这个对象。

4yield方法

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

5join方法

join方法有三个重载版本:

join()

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

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

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

2、实现Runnable接口创建多线程

· 适合多个相同的程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码、数据有效的分离,较好地体现了面向对象的设计思想

· 可以避免由于java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的之类放入多线程中,由于一个类不可能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable

· 当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是实现了Runnable接口的类的实例。

·  事实上,几乎所有多线程应用都可用Runnable接口方式。

 

通过继承Thread类实现多线程,但是这种方式有一定的局限性。因为在java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,比如学生类Student继承了person类,就无法再继承Thread类创建的线程。为了克服这种弊端,Thread类提供了另外一种构造方法ThreadRunnable target),其中Runnable是一个接口,它只有一个run()方法。当通过ThreadRunnable target)构造方法创建一个线程对象时,只需该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,二不需要调用Thread类中的run()方法。

1. package test;

2.  

3. public class example {

4.       public static void main(String[] args){

5.       MyThread myThread=new MyThread();

6.       Thread thread=new Thread(myThread);

7.       thread.start();

8.       while(true)

9.       {

10.       System.out.println("Main方法在运行");

11.       }

12.       }

13. }

14.  

15. class MyThread implements Runnable{

16. public void run(){

17. while(true){

18. System.out.println("MyThread类的run()方法在运行");

19. }

20. }

21. }

1.3 三种创建线程的办法

一、Java中创建线程主要有三种方式:

1、继承Thread类创建线程类

1)定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。

2)创建Thread子类的实例,即创建了线程对象。

3)调用线程对象的start()方法来启动该线程。

 

package com.thread;  

  

public class FirstThreadTest extends Thread{  

    int i = 0;  

    //重写run方法,run方法的方法体就是现场执行体  

    public void run()  

    {  

        for(;i<100;i++){  

        System.out.println(getName()+"  "+i);  

        }  

    }  

    public static void main(String[] args)  

    {  

        for(int i = 0;i< 100;i++)  

        {  

            System.out.println(Thread.currentThread().getName()+"  : "+i);  

            if(i==20)  

            {  

                new FirstThreadTest().start();  

                new FirstThreadTest().start();  

            }  

        }  

    }  

}

 

上述代码中Thread.currentThread()方法返回当前正在执行的线程对象。GetName()方法返回调用该方法的线程的名字。

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

1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。

2)创建 Runnable实现类的实例,并以此实例作为Threadtarget来创建Thread对象,该Thread对象才是真正的线程对象。

3)调用线程对象的start()方法来启动该线程。

示例代码为:

 

package com.thread;  

  

public class RunnableThreadTest implements Runnable  

{  

  

    private int i;  

    public void run()  

    {  

        for(i = 0;i <100;i++)  

        {  

            System.out.println(Thread.currentThread().getName()+" "+i);  

        }  

    }  

    public static void main(String[] args)  

    {  

        for(int i = 0;i < 100;i++)  

        {  

            System.out.println(Thread.currentThread().getName()+" "+i);  

            if(i==20)  

            {  

                RunnableThreadTest rtt = new RunnableThreadTest();  

                new Thread(rtt,"新线程1").start();  

                new Thread(rtt,"新线程2").start();  

            }  

        }  

    }   

}  

 

线程的执行流程很简单,当执行代码start()时,就会执行对象中重写的void run();方法,该方法执行完成后,线程就消亡了。

3、通过CallableFuture创建线程

1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。

public interface Callable

{

V call() throws Exception;

}

2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了FutureRunnable接口。)

3)使用FutureTask对象作为Thread对象的target创建并启动新线程。

4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

实例代码:

 

package com.thread;  

  

import java.util.concurrent.Callable;  

import java.util.concurrent.ExecutionException;  

import java.util.concurrent.FutureTask;  

  

public class CallableThreadTest implements Callable<Integer>  

{  

  

    public static void main(String[] args)  

    {  

        CallableThreadTest ctt = new CallableThreadTest();  

        FutureTask<Integer> ft = new FutureTask<>(ctt);  

        for(int i = 0;i < 100;i++)  

        {  

            System.out.println(Thread.currentThread().getName()+" 的循环变量i的值"+i);  

            if(i==20)  

            {  

                new Thread(ft,"有返回值的线程").start();  

            }  

        }  

        try  

        {  

            System.out.println("子线程的返回值:"+ft.get());  

        } catch (InterruptedException e)  

        {  

            e.printStackTrace();  

        } catch (ExecutionException e)  

        {  

            e.printStackTrace();  

        }  

  

    }  

  

    @Override  

    public Integer call() throws Exception  

    {  

        int i = 0;  

        for(;i<100;i++)  

        {  

            System.out.println(Thread.currentThread().getName()+" "+i);  

        }  

        return i;  

    }  

  

}  

 

二、创建线程的三种方式的对比

1、采用实现RunnableCallable接口的方式创建多线程时,

优势是:

线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。

在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势是:

编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。

2、使用继承Thread类的方式创建多线程时

优势是:

编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。

劣势是:

线程类已经继承了Thread类,所以不能再继承其他父类。

3RunnableCallable的区别

(1) Callable规定(重写)的方法是call()Runnable规定(重写)的方法是run()

(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。

(3) call方法可以抛出异常,run方法不可以。

(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

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

2.1 同步的概念和必要性

1. 概念:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程同步执行。

2. 线程同步的几种方式 
1互斥锁 
1.概念:实现线程访问临界资源的同步控制。 如果一个线程在临界区开始时,给互斥锁加锁, 那么其他的线程就必须等待线程解锁, 才能接着运行, 并访问资源。 
2.操作: 初始化, 加锁、 解锁、 销毁锁

锁初始化:

int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr);

加锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

2信号量 
1.信号量—-二进制信号量 
2.操作: 初始化, P 操作,V 操作, 销毁

  函数: #include <semaphore.h>

int sem_init(sem_t *sem, int shared, int val); 初始化

int sem_wait(sem_t *sem); P 操作

int sem_trywait(sem_t *sem);

int sem_post(sem_t *sem); V 操作

int sem_destroy(sem_t *sem); 销毁

3、 必要性

必要性:不管是多线程还是多进程,涉及到共享相同的内存时,需要确保好同步问题。对线程来说,需要确保每个线程看到一致的数据视图。如果每个线程使用的变量都是其他线程不会读取和修改的,那么就不存在一致性问题,同样的,如果变量是只读的,多个线程同时读取该变量也不会有一致性问题。但是如果其中的某个线程去改变该变量,其他线程也能读取或者修改的时候,我们就需要对这些线程进行同步,确保他们访问变量的存储内容时不会访问到无效的值。当线程修改变量的时候,其他线程在读取这个变量时可能会看到一个不一致的值,在变量修改时间多于一个存储器访问周期的处理器结构中,当存储器读与写这两个周期交叉,不一致就会出现。

2.2 synchronize关键字和同步块

一、临界区 Critical Section

临界区是一个用以访问共享资源的代码块,这个代码块在同一时间内只允许一个线程执行

二、同步

1、当一个线程试图访问一个临界资源时,它将使用一种同步机制来查看是不是已经有其他的线程进入临界区

2、如果临界区没有其他线程,它就可以进入临界区。如果已经有线程进入临界区,它就被同步机制挂起,直到进入的线程离开这个临界区

3、如果在等待进入临界区的线程不止一个,jvm就会选择其中一个,其他的继续等待

三、java采用的两种基本的同步机制

synchronized关键字机制

Lock接口及其实现

四、synchronize关键字机制

synchronize修饰方法

1、使用synchronize关键字的方法性能比较低

2、每一个使用synchronize修饰的方法都是临界区

3、使用synchronize修饰的对象,那么同一时间只能有一个执行线程访问,如果其他线程试图访问这个对象的其他方法,都将被挂起

4、synchronize修饰静态方法会怎么样?

使用synchronize修饰的静态方法,那么同一时间只能有一个执行线程访问,但是其他线程可以访问这个对象的非静态方法。

synchronize修饰代码块

1、方法的其余部分保持在synchronize代码块之外,以获取更好的性能

2、synchronized修饰代码块的参数是当前对象,注意是对象,不是类

2.3 实例

package zheng;

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

public class testhread {

static int c=0;

static Object lock = new Object();

public static void main(String[] args) {

Thread[] thread = new Thread[1000];

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

final int index = i;

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

synchronized(lock) {

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

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

a++;//将值加一

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

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

}

catch(InterruptedException e) {

e.printStackTrace();

}

c=a;//存回去

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

}

});

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

}

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

try {

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

}catch(InterruptedException e) {

e.printStackTrace();

}

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

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

 

}

}

3 生产者消费者问题

3.1 问题表述

1:生产者-消费者问题描述:

有一组生产者的线程和一组消费者的线程共享一个初始为空,大小为n的缓冲区,如果缓冲区未满,那么生产者就可以把生产的放进缓冲区里,如果缓冲区不空,那么消费者就可以从缓冲区取出来消费; 问题分析: * 关系分析:生产者和消费者对缓冲区的互斥访问是互斥关系,同时生产者和消费者又是一个协作的关系,之后生产者生产了之后,消费者才可以消费,他们是同步关系。

3.2 实现思路

我们这里利用一个一个数组buffer来表示这个n个缓冲区的缓冲池,用输入指针和输出指针+1来表示在缓冲池中存入或取出一个产品。由于这里的缓冲池是循环缓冲的,故应把inout表示成:in = ( in +1 ) % n (或把out表示为 out = ( out +1 ) % n )( in +1) % n= out的时候说明缓冲池满,in = out 则说明缓冲池空。在这里还要引入一个整型的变量counter(初始值0),每当在缓冲区存入或取走一个产品时,counter +1-1。那么问题的关键就是,把这个counter作为临界资源处理,即令生产者进程和消费者进程互斥的访问它。

3.3 Java实现该问题的代码

package org.young;

 

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 总结

通过这次课程实践使我懂得了理论与实际相结合是很重要的,只有理论知识是远远不够的,只有把所学的理论知识与实践相结合起来,从理论中得出结论,才能提高自己的实际动手能力和独立思考的能力。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值