并发中的进程与线程(1)

并发

进程与线程

文章目录

  • 并发
    • 进程与线程
      • 1.进程
        • 1.1 进程的概念
        • 1.2 进程的特点
      • 2.线程
        • 2.1 线程的概念
        • 2.2 线程与进程的关系
      • 3.多线程的特性
        • 3.1 随机性
            • 串行与并行
        • 3.2 CPU分时调度
        • 3.3 线程的状态
        • 3.4 线程的属性
          • 3.4.1 中断线程
          • 3.4.2 守护线程
      • 4.多线程代码对象创建
        • 4.1 继承Thread类
          • 4.1.1 概述
        • 4.2 实现Runnable接口
          • 4.2.1 概述
          • 4.2.2 常用方法
          • 4.2.3 测试代码
          • 4.2.4 小结

1.进程

1.1 进程的概念

进程就是正在运行的程序,它会占用对应的内存区域,由CPU进行执行与计算。

1.2 进程的特点
  1. 独立性
    进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间

  2. 动态性
    进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.

  3. 并发性

    多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.并发执行的进程数目并不受限于CPU数目。操作系统会为每个进程分配CPU时间片,给人并行处理的感觉。

2.线程

2.1 线程的概念

线程是控制线程的简称。线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.如果一个进程可以同时运行多个线程,则称这个进程是多线程的(multithreaded)。一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
我们看到的进程的切换,切换的也是不同进程的主线程
多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。

2.2 线程与进程的关系

一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)

img

每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成

img

多线程与多进程的区别:每个线程都拥有自己的一整套变量,而线程则共享数据。共享变量使线程之间的通信比进程更有效、更容易。此外,在有些操作系统中,与进程相比较,线程更"轻量级",创建、撤销一个线程比启动新进程的开销要小得多。

3.多线程的特性

3.1 随机性

我们宏观上觉得多个进程是同时运行的,但实际的微观层面上,一个CPU【单核】只能执行一个进程中的一个线程
那为什么看起来像是多个进程同时执行呢?
是因为CPU以纳秒级别甚至是更快的速度高效切换着,超过了人的反应速度,这使得各个进程从看起来是同时进行的,也就是说,宏观层面上,所有的进程看似并行【同时运行】,但是微观层面上是串行的【同一时刻,一个CPU只能处理一件事】。

线程切换
串行与并行

串行是指同一时刻一个CPU只能处理一件事,类似于单车道
并行是指同一时刻多个CPU可以处理多件事,类似于多车道

在这里插入图片描述 img
3.2 CPU分时调度

时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:

  • FCFS(First Come First Service 先来先服务算法)
  • SJS(Short Job Service 短服务算法)
img

在有多个处理器的机器上,每一个处理器运行一个线程,可以有多个线程并行运行。当然,如果线程的数目多于处理器的数目,调度器还是需要分配时间片。

3.3 线程的状态

由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :

  • 可运行状态:线程已经准备好运行,只要获得CPU,就可立即执行
  • 执行(运行)状态:线程已经获得CPU,其程序正在运行的状态
  • 阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞
线程的3种状态
就绪 → 执行:为就绪线程分配CPU即可变为执行状态"
执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态
执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞
(例如线程正在访问临界资源,而资源正在被其他线程访问)
反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行

我们可以再添加两种状态:

  • 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
  • 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
img

PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程

img

线程生命周期,主要有五种状态:

  1. 新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();

  2. 可运行状态(Runnable):当调用线程对象的start()方法,线程即为进入可运行状态.
    处于状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行,要由操作系统为线程提供具体的执行时间。

  3. 运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
    就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态(不过,Java规范并没有把正在运行状态作为一个单独的状态,一个正在运行的线程仍然处于可运行状态)

注:一旦一个线程开始运行,它不一定始终保持运行。事实上,运行中的线程有时需要暂停,让其他线程有机会运行。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完后,操作系统剥夺该线程的运行权,并给另一个线程一个机会来运行。当选择下一个线程时,操作系统会考虑线程的优先级。

  1. 阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.

    根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:

    等待阻塞:当线程等待另一个线程通知调度器出现一个条件,或当该线程调用wait()方法时,这个线程会进入等待状态。有几个方法有超时参数,调用这些方法会让线程进入计时等待(time waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。

    同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态。当所有其他线程都释放了这个锁,并且线程调度器运行该线程持有这个锁时,它将变成非阻塞状态。

    其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态

  2. 终止状态(Dead):run方法正常退出或者因为一个没有捕获的异常终止了run方法使线程意外终止,该线程结束生命周期。具体来说,可以调用线程的stop方法杀死一个线程。该方法抛出一个ThreadDeath错误对象,这会杀死线程。不过,stop方法已经废弃,不要在你的代码中调用该方法。

注:当一个线程阻塞或等待时(或终止时),可以调度另一个线程运行。当一个线程被重新激活(例如,因为超时期满或成功地获得了一个锁)调度器检查它是否具有比当前运行线程更高的优先级。如若这样,调度器会剥夺某个当前运行线程的运行权,选择一个新线程运行。

image-20220311175032745
3.4 线程的属性
3.4.1 中断线程

当该线程的run方法执行方法体中最后一条语句后再执行return语句返回时,或者出现了一个没有捕获的异常时,线程将终止。

除了已经废弃的stop方法,没有办法可以强制线程终止。不过,interrupt方法可以用来请求终止一个线程。当对一个线程调用interrupt方法时,就会设置线程的中断状态。这是每个线程都有的boolean标志。每个线程都应不时地检查这个标志,以判断线程是否被中断。可以通过调用

Thread.currentThread().isInterrupted()

但如果线程被阻塞,就无法检查中断状态。当在一个被sleep或wait调用阻塞的线程上调用interrupt方法时,将会抛出InterruptedException异常。

注: 没有任何语言要求被中断的线程应当终止。中断一个线程只是要引起它的注意。被中断的线程可以决定如何响应中断。某些线程非常重要,所以应该处理这个异常,然后再继续执行。但是,更普遍的情况是,线程只希望将中断解释为一个终止请求。

如果设置了中断状态,此时倘若调用sleep方法,它不会休眠,而是会清除中断状态并抛出InterruptedException异常。因此,如果你的循环调用了sleep,不要检测中断状态,而应该捕获InterruptedException异常。

API java.lang.Thread
void interrupt()
向线程发送中断请求。线程的中断状态将设置为true。如果当前该线程被一个sleep调用阻塞,则抛出一个InterruptedException异常。

static boolean interrupt()
测试当前线程(即正在执行这个指令的线程)是否被中断。这个调用有一个副作用——它将当前线程的中断状态重置为false。

boolean isInterrupted()
与static interrupt不同,这个调用不改变线程的中断状态。

static Thread currentThread()
返回表示当前正在执行的线程的Thread对象
3.4.2 守护线程

可以通过调用

t.setDaemon(ture)

将一个线程转换为守护线程(daemon thread)。守护线程的唯一用途是为其他线程提供服务。例如计时器线程,它定时发送"计时器嘀嗒"信号给其他线程,另外清空过时缓存项的线程也是守护线程。当只剩下守护线程时,虚拟机就会退出。因为如果只剩下守护线程,就没必要继续运行程序了。

API java.lang.Thread
void setDaemon(boolean isDaemon)
标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用

4.多线程代码对象创建

4.1 继承Thread类
4.1.1 概述

Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例
启动线程的唯一方法就是通过Thread类的start()实例方法
start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
这种方式实现的多线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法
模拟开启多个线程,每个线程调用run()方法.

4.2 实现Runnable接口
4.2.1 概述

如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口

4.2.2 常用方法
API  java.lang.Thread 

构造方法

Thread() 构造一个新线程
Thread(String name) 构造一个新线程,并设置该Thread对象名
Thread(Runnable target) 构造一个新线程,调用指定目标的run()方法
Thread(Runnable target,String name) 构造一个新线程,调用指定目标的run()方法,并设置该Thread对象名

普通方法

static Thread currentThread( )
返回对当前正在执行的线程对象的引用
void setName(String name)
调用该方法为线程设置名字,在线程转储中可能很有用
long getId()
返回该线程的标识
String getName()
返回该线程的名称
void run()
调用相关Runnable的run方法
void start()
使该线程开始执行:Java虚拟机调用该线程的run()。启动这个线程,从而调用run()方法。这个方法会立即返回。新线程会并发运行
static void sleep(long mills)
让该线程(执行状态)休眠指定的毫秒数(暂停执行)
API  java.lang.Runnable
void run()
必须覆盖这个方法,提供你希望执行的任务指令
4.2.3 测试代码

import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;
    /**
     * Constructs the bank
     * @param n the number of accounts
     * @param initialBalance the initial balance for each account
     */
    public Bank(int n,double initialBalance)
    {
        accounts=new double[n];
        Arrays.fill(accounts,initialBalance);
        bankLock=new ReentrantLock();
        sufficientFunds=bankLock.newCondition();
    }

    /**
     * Transfers money from one account to another
     * @param from the account to transfer from
     * @param to to the account to transfer to
     * @param amount the amount to transfer
     */
    public void transfer(int from,int to,double amount) throws InterruptedException {
        while (accounts[from] < amount) {
            System.out.println("因 accounts["+from+"] ("+accounts[from]+")< amount ("+amount+") "+Thread.currentThread().getName()+"进入WAIT状态");
            wait();
        }
        System.out.println(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        System.out.println("重新激活所有线程");
        notifyAll();
    }

    /**
     * Gets the sum of all account balance
     * @return the total balance
     */
    public double getTotalBalance()
    {
        try {
            double sum = 0;
            for (double a : accounts)
                sum += a;
            return sum;
        }
    }

    public void getEachBalance()
    {

        for (int i=0;i<size();i++)
        {
            System.out.println(accounts[i]);
        }
    }

    /**
     * Gets the number of accounts in the bank
     * @return the number of accounts
     */
    public int size()
    {
        return accounts.length;
    }
}


public class ThreadTest {
    public static final int DELAY = 10;
    public static final int STEPS = 20;
    public static final double INITIAL_BALANCE = 10000;
    public static final double MAX_AMOUNT = 20000;
/**
 * 线程的并发运行测试
 */
    public static void main(String[] args) {

        var bank = new Bank(4, INITIAL_BALANCE);
        Runnable task1 = () -> {
            try {
                Thread.currentThread().setName("Thread-1");
                for (int i = 0; i < STEPS; i++) {
                    double amount = MAX_AMOUNT * Math.random();
                    bank.transfer(1, 2, amount);
                    Thread.sleep((int) (DELAY));
                }
                System.out.println(" task1 :");
                bank.getEachBalance();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        Runnable task2 = () -> {
            try {
                Thread.currentThread().setName("Thread-2");
                for (int i = 0; i < STEPS; i++) {
                    double amount = MAX_AMOUNT * Math.random();
                    bank.transfer(2, 1, amount);
                    Thread.sleep((int) (DELAY ));
                }
                System.out.println(" task2 :");
                bank.getEachBalance();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        };

        //以多线程的方式启动任务1,将当前线程变为就绪状态
        //执行的时候start()底层会自动调用我们给定Runnable对象重写的run()方法,因Runnable为函数式接口,可用lambda表达式来重写run()方法
        new Thread(task1).start();
        new Thread(task2).start();

    }
}

警告:不要调用Thread类或Runnable对象的run方法。直接调用run方法只会在同一个线程中执行这个任务——而没有启动新的线程。实际上,应当调用

Thread.start()

这会创建一个执行run方法的新线程

:我可以通过建立Thread类的一个子类开定义线程,构造这个子类的一个对象并调用它的start方法。不过,现在不再推荐这种方法。应当把要并行运行的任务与运行机制解耦合。如果有多个任务,为每个任务分别创造一个单独的线程开销会太大。实际上,可以使用一个线程池

4.2.4 小结

在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。如果两个线程存取同一个对象,并且每个线程分别调用了一个修改该对象状态的方法,会发生什么呢?可以想见,这两个线程会相互覆盖。这有可能破坏共享数据。在这里的测试程序运行时,可以清楚地看见余额有轻微变化,有时可能需要很长时间才能发现这个错误。在现实的银行存取中,你肯定不希望看到自己的余额莫名其妙便少了,当然也有可能变多(可以试试)。为了防止这种不稳定情况出现我们需要防止并发访问这块代码,一种是使用synchronnized关键字,另外也可以使用。这一部分请参阅我的下一篇文章。
并发中的同步

你可能感兴趣的:(java,开发语言)