zoukankan      html  css  js  c++  java
  • 并发与多线程

    并发

      并发(concurrency)是指CPU在某个时间段内交替处理多任务的能力。每个CPU不可能只顾着执行某个进程,而让其他进程一直等待被执行。所以,CPU把可执行时间均分成若干份,每个进程执行一份或多份时间后,记录当前的工作状态,释放相关资源并进入等待状态,让其他进程抢占CPU等资源。

      在并发环境下,由于程序的封闭性被打破,出现了以下特点:

      1.并发程序之间有相互制约的关系。直接制约体现在一个程序需要另一个程序的计算结果;间接制约体现在多个进程竞争共享资源。

      2.并发程序的执行过程是断断续续的。程序需要保留现场,记忆现场指令及执行点。

      3.当并发数设置合理并且CPU拥有足够的处理能力时,并发会提高程序的运行效率。

      在Java编程中,并发主要与线程有关。

    线程

      线程是CPU调度和分派的基本单位,为了更充分地利用CPU资源,一般都会使用多线程进行处理。多线程的作用是提高任务的平均执行速度,但是会导致程序可解性变差,编程难度加大。所以,合适的线程数才能让CPU资源被充分利用。

      每一个线程都有自己的操作栈、程序计数器、局部变量表等资源。同一进程内的所有线程都可以共享该进程的所有资源。

      Java提供了两种形式定义线程类:

      1.实现Runnable接口并重写其中的run()方法。

     1 class Consumer implements Runnable {
     2 
     3     private Store store;
     4 
     5     public Consumer(Store store) {
     6         this.store = store;
     7     }
     8 
     9     @Override
    10     public void run() {
    11         for (int i = 0; i < 1000; i++) {
    12             store.getValue();
    13         }
    14     }
    15 
    16 }
    Consumer

      2.继承Thread类并重写其中的run()方法。

     1 class Producer extends Thread {
     2 
     3     private Store store;
     4 
     5     public Producer(Store store) {
     6         this.store = store;
     7     }
     8 
     9     @Override
    10     public void run() {
    11         for (int i = 0; i < 1000; i++) {
    12             store.setValue((int) (Math.random() * 100));
    13         }
    14     }
    15 
    16 }
    Producer

      里氏代换原则对继承的一个约束是子类不重写父类的非抽象方法,而Thread类的run()方法不是一个抽象方法,所以继承Thread类并重写其中的run()方法就不符合里氏代换原则,该方式不推荐使用。相比之下,实现Runnable接口可以使编程更加灵活,对外暴露的细节也比较少,让使用者专注于实现线程的run()方法。

    线程状态

      线程的生命周期分为以下5种状态:

    新建状态

      新建状态是线程被创建且未启动的状态。也就是说,初始化一个线程对象时,该对象进入新建状态。

      线程对象的初始化分为2种:

      1.如果是继承Thread类的线程类,则该类线程对象可以直接通过new运算进行初始化。

      2.如果是实现Runnable接口的线程类,则该类线程对象通过new运算进行初始化后需要包装为一个Thread对象。

    就绪状态

      就绪状态是线程启动后运行之前的状态。即启动了的线程在准备执行run()方法时的状态。

      线程的启动是指线程对象调用Thread的start()方法。

    运行状态

      运行状态是线程运行时的状态,即启动了的线程在执行run()方法时的状态。

    阻塞状态

      阻塞状态分以下3种情况:

      同步阻塞:缺少资源无法继续运行。抢占到资源后会退出该状态。

      主动阻塞:主动让出CPU执行权,即线程执行Thread的sleep()方法之后的状态。调用sleep()方法时会传入一个long类型的参数,表示睡眠的时间,单位为毫秒,时间结束时会退出该状态。

      等待阻塞:进入睡眠,即线程执行Object的wait()方法之后的状态。其他线程执行Object的notify()方法或notifyAll()方法之后会退出该状态。

    终止状态

      终止状态是线程执行结束或因异常退出后的状态。

    线程同步

      线程同步机制的主要任务是,对多个相关线程在执行次序上进行协调,使并发执行的每个线程之间能按照一定的时序共享资源,并能很好地相互合作,从而使程序的执行具有可再现性。

      资源的共享分为两种方式:

      互斥共享方式:某些资源例如打印机、磁带机等,一次只能给一个线程使用,当一个线程申请该资源时,如果该资源有其他线程在使用,则该线程需要等待,直到资源被释放之后才能申请。

      同时访问方式:某些资源例如磁盘设备等,一次可以给多个线程“同时”访问,这种“同时”是宏观上的,实际上还是多个线程交替访问。

      临界资源指的是一段时间内只能由一个线程访问的资源,而临界区指的是每个线程中访问临界资源的那部分代码。显然,若能保证每个线程互斥地进入自己的临界区,便可以实现每个线程对临界资源的互斥访问。为此,需要在每个线程进入临界区前需要对访问的临界资源进行检查,如果它是空闲的,则进入临界区;否则等待,直到临界资源空闲。具体流程如下:

      进入区:检查临界资源的状态,如果空闲,则将其状态改为被访问,并进入临界区;如果被访问,则循环等待,直到其状态变为空闲。

      临界区:访问临界资源。

      退出区:将临界资源的状态改为空闲,并释放临界资源。

      Java提供synchronized关键字标识方法或代码块,被标识的方法称为同步方法,被标识的代码块称为同步代码块。每个对象都有一个监视器与之关联。当线程通过该对象执行同步方法或同步代码块时,它首先试图获取监视器,如果获取到监视器,则锁定该对象,防止其他线程通过该对象执行同步方法或同步代码块,执行结束后,解锁该对象并释放监视器;如果获取不到监视器,表示有其他线程通过该对象执行同步方法或同步代码块,则会进入等待。所以,监视器的作用就相当于进入区和退出区的作用。

      例如:定义两种线程——生产者(Producer)和消费者(Consumer),生产者每次会产生一个数,消费者每次会取出一个数。Producer和Consumer线程对象通过同一个Store对象来调用Store的同步方法。

     1 class Store {
     2 
     3     private int value;
     4 
     5     public synchronized int getValue() {
     6         System.out.println("-取出" + value);
     7         return value;
     8     }
     9 
    10     public synchronized void setValue(int value) {
    11         this.value = value;
    12         System.out.println("放入" + value);
    13     }
    14 
    15 }
    Store
    1 @Test
    2 void test() {
    3     Store store = new Store();
    4     Thread producer = new Producer(store);   // 继承Thread类的线程类对象的初始化
    5     Thread consumer = new Thread(new Consumer(store));   // 实现Runnable接口的线程类对象的初始化
    6     producer.start();
    7     consumer.start();
    8 }
    test

      部分输出结果:

      

      当Consumer线程对象调用getValue()方法时,会获取监视器,锁定Store对象,直到方法返回后解锁Store对象,释放监视器;当Producer线程对象调用setValue()方法时也是如此。所以在创建Producer和Consumer线程对象时需要传入同一个Store对象。如果传入不同的Store对象,每一个Store对象都有一个监视器,则起不到锁定的效果。

      根据输出结果可以发现:取出多次数后才放入一次数,放入多次数后才取出一次数。要实现放入一个数后取出一个数的效果,则需要添加一个标识量。

      改进:在Store类中添加一个mutex标识量,当mutex为true时,表示Store内存了一个数,等待Consumer来取;为false时,表示Store内没有数,等待Producer生产数。

     1 class Store {
     2 
     3     private int value;
     4     private boolean mutex;   // mutex初始值为false,表示没有数
     5 
     6     public synchronized int getValue() {
     7         while (! mutex) {   // mutex为false时进入等待
     8             try {
     9                 wait();
    10             } catch (InterruptedException e) {
    11                 e.printStackTrace();
    12             }
    13         }
    14         System.out.println("-取出" + value);
    15         mutex = false;   // 取出数后将mutex置为false
    16         notify();
    17         return value;
    18     }
    19 
    20     public synchronized void setValue(int value) {
    21         while (mutex) {   // mutex为true时进入等待
    22             try {
    23                 wait();
    24             } catch (InterruptedException e) {
    25                 e.printStackTrace();
    26             }
    27         }
    28         this.value = value;
    29         System.out.println("放入" + value);
    30         mutex = true;   // 放入数后将mutex置为true
    31         notify();
    32     }
    33 
    34 }
    Store

      部分输出结果:

      

  • 相关阅读:
    Java抽象类、接口能否有构造方法
    Java堆溢出、栈溢出示例
    typora常用快捷键
    什么是业务逻辑
    解决idea登录github出现的invalid authentication data 404 not found
    SQL常用聚合函数
    oracle存储过程/函数调试
    解决IDEA全局搜索Ctrl+Shift+F失效问题
    如何在win10系统中使用Linux命令
    Java复现NullPointerException异常
  • 原文地址:https://www.cnblogs.com/lqkStudy/p/11135153.html
Copyright © 2011-2022 走看看