操作系统实践

1 线程的创建与启动

1.1 进程与线程

进程和线程的区别在于:

简而言之,一个程序至少有一个进程,一个进程至少有一个线程

线程的划分尺度小于进程,使得多线程程序的并发性高。 
另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位. 
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源. 

一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.

1.2 Java中的ThreadRunnable

 在实现Runable接口的时候调用Thread的Thread(Runnable run)或者Thread(Runnable run ,String name)构造方法创建进程时,使用同一个Runnable实例,如上程序中使用的都是rt,则建立的多线程的实例变量也是共享的;
但是通过继承Thread类是不能用一个实例建立多个线程;
故而实现Runnable接口适合于资源共享;
当然,继承Thread类也能够共享变量,能共享Thread类的static变量

1.3 三种创建线程的办法

第一种方法:继承Thread,重写run()方法,run()方法代表线程要执行的任务。
第二种方法:实现Runnable接口,重写run()方法,run()方法代表线程要执行的任务。
第三种方法:实现callable接口,重写call()方法,call()作为线程的执行体,具有返回值,并且可以对异常进行声明和抛出

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

2.1 同步的概念和必要性

同步就是协同步调,按预定的先后次序进行运行。

在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

2.2 synchronize关键字和同步块

synchronizedJava中的关键字,是一种同步锁。它修饰的对象有以下几种: 

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

2.3 实例

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(); //启动了线程 
}

}

3 生产者消费者问题

3.1 问题表述

生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

3.2 实现思路

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

3.3 Java实现该问题的代码

1).生产者跟消费者之间消费的是产品,所有先定义一个产品类:
public class Product {
    //定义产品的唯一ID
    int id;
    //定义构造方法初始化产品id
    public Product(int id) {
        this.id=id;
        // TODO Auto-generated constructor stub
    }
}
(2).定义一个仓库用来存放产品.
public class Repertory {
    //定义一个集合类用于存放产品.规定仓库的最大容量为10.
    public LinkedList<Product> store=new LinkedList<Product>();
    public LinkedList<Product> getStore() {
        return store;
    }
    public void setStore(LinkedList<Product> store) {
        this.store = store;
    }
    /* 生产者方法
     * push()方法用于存放产品.
     * 参数含义:第一个是产品对象
     * 第二个是线程名称,用来显示是谁生产的产品.
     * 使用synchronized关键字修饰方法的目的:
     * 最多只能有一个线程同时访问该方法.
     * 主要是为了防止多个线程访问该方法的时候,将参数数据进行的覆盖,从而发生出错.
     */
    public synchronized void push(Product p,String threadName)
    {
        /* 仓库容量最大值为10,当容量等于10的时候进入等待状态.等待其他线程唤醒
         * 唤醒后继续循环,等到仓库的存量小于10时,跳出循环继续向下执行准备生产产品.
         */
        while(store.size()==10){
            try {
                //打印日志
                System.out.println(threadName+"报告:仓库已满--->进入等待状态--->呼叫老大过来消费");
                //因为仓库容量已满,无法继续生产,进入等待状态,等待其他线程唤醒.
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //唤醒所有等待线程
        this.notifyAll();
        //将产品添加到仓库中.
        store.addLast(p);
        //打印生产日志
        System.out.println(threadName+"生产了:"+p.id+"号产品"+" "+"当前库存来:"+store.size());
        try {
            //为了方便观察结果,每次生产完后等待0.1秒.
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
         
    }
    /* 消费者方法
     * pop()方法用于存放产品.
     * 参数含义:线程名称,用来显示是谁生产的产品.
     * 使用synchronized关键字修饰方法的目的:
     * 最多只能有一个线程同时访问该方法.
     * 主要是为了防止多个线程访问该方法的时候,将参数数据进行的覆盖,从而发生出错.
     */
    public synchronized void pop(String threadName){
        /* 当仓库没有存货时,消费者需要进行等待.等待其他线程来唤醒
         * 唤醒后继续循环,等到仓库的存量大于0时,跳出循环继续向下执行准备消费产品.
         */
        while(store.size()==0)
        {
            try {
                //打印日志
                System.out.println(threadName+"下命令:仓库已空--->进入等待状态--->命令小弟赶快生产");
                //因为仓库容量已空,无法继续消费,进入等待状态,等待其他线程唤醒.
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //唤醒所有等待线程
        this.notifyAll();
        //store.removeFirst()方法将产品从仓库中移出.
        //打印日志
        System.out.println(threadName+"消费了:"+store.removeFirst().id+"号产品"+" "+"当前库存来:"+store.size());
        try {
            //为了方便观察结果,每次生产完后等待1秒.
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
(3).定义生产者
public class Producer implements Runnable {
    //定义一个静态变量来记录产品号数.确保每一个产品的唯一性.
    public static  Integer count=0;
    //定义仓库
    Repertory repertory=null;
    //构造方法初始化repertory(仓库)
    public Producer(Repertory repertory) {
        this.repertory=repertory;
    }
    /* run()方法因为该方法中存在非原子性操作count++;
     * 当多个线程同时访问时会发生count++的多次操作,导致出错
     * 为该方法添加同步错做,确保每一次只能有一个生产者进入该模块。
     * 这样就能保证count++这个操作的安全性.
     */
    @Override
    public void run() {
            while (true) {     
                synchronized(Producer.class){
                    count++;
                    Product product=new Product(count);
                    repertory.push(product,Thread.currentThread().getName());
                }
                             
             
        }
         
    }
}
(4).定义一个消费者
public class Consumer implements Runnable {
    //定义仓库
    Repertory repertory=null;
    //构造方法初始化repertory(仓库)
    public Consumer(Repertory repertory) {
        this.repertory=repertory;
    }
    //实现run()方法,并将当前的线程名称传入.
    @Override
    public  void run() {
        while(true){
            repertory.pop(Thread.currentThread().getName());
        }
    }
 
}
(5).测试类
public class TestDemo {
  public static void main(String[] args) {
    //定义一个仓库,消费者和生产者都使用这一个仓库
    Repertory repertory=new Repertory();
    //定义三个生产者(p1,p2,p3)
    Producer p1=new Producer(repertory);
    Producer p2=new Producer(repertory);
    Producer p3=new Producer(repertory);
    //定义两个消费者(c1,c2)
    Consumer c1=new Consumer(repertory);
    Consumer c2=new Consumer(repertory);
    //定义5个线程(t1,t2,t3,t4,t5)
    Thread t1=new Thread(p1,"张飞");
    Thread t2=new Thread(p2,"赵云");
    Thread t3=new Thread(p3,"关羽");
    Thread t4=new Thread(c1,"刘备");
    Thread t5=new Thread(c2,"曹操");
    //因为关羽跟赵云的生产积极性高,所以把他们的线程优先级调高一点
    t2.setPriority(10);
    t3.setPriority(10);
    //启动线程
    t1.start();
    t2.start();
    t3.start();
    t4.start();
    t5.start();
  }
}

3.4 测试

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

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

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

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

4 总结

继承Thread类的方法创建线程类的时候,多个线程不能共享线程类的实例变量,而实现Runnable接口可以,因为其只是作为target传入Thread,多个线程共享一个target,那么线程的实例属性的变化是连续的。(验证的时候注意,这里说的线程不包括主线程执行的结果是连续的,只是start启动的线程的实例变量变化连续(不是局部变量))。

生产者和消费者问题从单个到多个,从同步问题到互斥问题,只有真正分析清楚他们之间的同步互斥变量,才能做到不放生死锁,本文首先通过对问题的分析,抽象出同步变量、互斥变量,再进行操作,使问题得到解决,并对其中所提问题做出解释。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值