进程与线程关系:从0开始讲清楚

sda

本文主要阐释进程与进程之间的关系以及区别,以及如何通过java代码在系统中创建一个线程,以及java层面的线程与系统中线程的关系

一.了解并发编程

     在进入本文内容我们首先需要知道,什么是串行,什么是并发,什么是并行,什么是并发编程?如果知道可以跳过这部分内容。

串行:就是做一件事情完成后,再去做另一件事情,一件一件完成

并发:并发就是一会做这个事情(还没有完成),一会做另一个事情(还没有完成),多个事情轮换交替做,在cpu单核中,cpu快速的轮换执行多个任务,速度极快,让我们感觉好像是在同时做两个事情,但是并不是同时,只是快速轮换。

并行:并行就是一边在做这个事情,一边在做那个事情,我们的计算计是多核的,他是可以同时执行多个指令,那么并行就是两个任务,同时在cpu多个核心上同时执行,是真正的同时执行

在开发中我们并不区分,并发和并行,我们将他们统称为并发编程

使用多线程编程,通过cpu调度,让线程在多个核心上运行,极大提高了运行效率(不了解线程是什么,可以看完进程与线程是什么,为什么要使用线程而不是进程回来看这句话)

二.进程与线程是什么

     首先我们需要了解什么是进程,以及什么是线程?

进程:进程其实是运行中的应用程序(应用程序在下载后他是静态的躺在磁盘中,而当双击.exe文件启动后,操作系统为其创建一个 进程 ,然后会在内存中划出一片内存空间给该进程,并为该进程分配资源,资源包括内存资源以及进程所需要的其他文件资源)。

线程: 线程也可以叫做轻量级进程(当然他并不是进程),当一个进程被创建的时候,会自动创建一个主线程,而参与cpu调度的其实并不是进程,而是进程中的线程。

由此得出两个重要的概念 

**  进程是系统分配资源的最小单位

**  线程是参与cpu调度的最小单位

以上的描述可能并不能让你理清进程与线程的概念,以下有个形象的例子来帮你理清他们之前的关系

场景:张三想要开一个工厂

那么他需要做这些准备

我们将这个例子与系统中一一对应起来

由图我们可以很清晰的看出 进程就像一个工厂,在建工厂前需要很多准备 ,所以进程是系统分配资源的最小单位。  线程就像是一条流水线,那么显而易见,线程才是真正参与工作的,即线程是参与cpu调度的最小单位。线程我们需要关注其执行任务,在此场景中生产皮包即是他的任务

还有一个问题我们可以思考一下,我们是不是可以发现,进程与进程相对独立(假设张三的工厂停电了,那么李四的工厂和其他人的工厂并不会出现问题,每个工厂的电路都是自己排好的),线程与线程之间就容易相互影响,毕竟处于一个工厂中,使用的资源都是来自一样的)这涉及了线程安全的问题,在这里就不讨论。

三.为什么要使用线程而不是进程

我们理清了线程与进程的关系,那么有个问题,我们为什么要多此一举用线程,而不是直接用进程呢?

假设:张三的工厂生产的是皮包,现在张三的工厂在一条流水线的情况下,每天只能生产100个皮包,此时张三的工厂生意越来越好,每天生产的量跟不上卖的速度了,那么如何来提高生产效率

现在有两个方案

1:再开一个工厂,搭建一条流水线

2:直接在目前的工厂中,搭建第二条流水线

显而易见,我们会选择第二个方案,第二个方案的执行成本比第一个方案的执行成本低了很多

此时就发现了线程显而易见的一个优势,线程不管是创建的成本还是销毁的成本,都远低于进程,且在一个进程中,所有的线程共享同一个进程的资源。

总结一下线程的优势:

1.线程不管是创建的成本还是销毁的成本,都远低于进程

2.一个进程中,所有的线程共享同一个进程的资源。

3.多线程相比多进程的效率更高(且进程其实正在参与调度的也是主线程,而且进程与进程之间的通信相比一个进程中多线程的通信更加复杂,效率慢)

那么接下来还有另一个例子来解释线程与进程关系,以及一些其他问题

场景:现在在一个房子里面有一个桌子桌子上面有100份地瓜,需要把他吃完

此时房子则是对应着整个进程,桌子还有地瓜都是资源,火柴人则是一个线程(主线程),线程的任务则是吃完100个地瓜

使用多进程的情况:

   我们使用多进程,再建一个房子和桌子,同时要再创建一个人,然后将吃地瓜的任务平均分配给  两个人,速度相比之下一定是提升了,但是消耗的资源也变多了许多,明明只要让两个人围着一个桌子吃饭就好了,这样子做不是很明显的小题大作吗

使用多线程的情况:

此时这种方案是不是明显合理多了,那么此时我们一定已经可以理解为什么我们使用多线程,而不是多进程了吧。当然还有一些问题我们需要注意,往下看

当我们让更多人来吃饭:

此时我们叫了四个人来吃饭,当然速度一定还是更快

当我们叫了更多的人:

此时人数非常非常多,甚至有人已经排到了屋子外面。

我们执行程序时候创建一个进程,进程里面有多个线程,cpu中有多个核心,那么这些线程就会参与cpu的调度进入各个核心执行任务,此时就是一个多线程执行一个程序,可是我们cpu的核心有限,

当线程数量 <  逻辑处理器时候这时候速度会提高,但是当                  线程远大于  > >逻辑处理器时候,这时候有许多线程都是阻塞等待的状态,并不能起到真正的并发编程效果,反而创建了更多的线程造成了资源的浪费.

当然还有一些其他情况:

当此时只剩一个地瓜的时候就有可能出现争抢资源,而当一个线程抢不过另一个线程崩溃了,因为线程之间容易相互影响,有可能导致整个进程崩溃。

四.进程与线程的关系(区别)总结

此时我们应该已经了解了大概关于进程与线程

总结:

1.进程包含着线程,一个进程创建时候就会自动创建一个主线程

2.进程是系统分配资源的最小单位

3.线程是参与cpu调度的最小单位

4.一个进程内,所有线程共享资源

5.进程之间相互独立(MMU单元)

6.线程之间容易相互影响

五.在java中如何创建的线程

了解以后,我们就来说如何使用java代码来在系统中创建一个线程,我们先来看看如何创建看看现象,看完在解释其中的java层面的线程与系统中线程的关系

public class Main {
    public static void main(String[] args) {
        //用我们创建的线程类,new一个对象出来,此时相当于java层面的线程被创建但并未启动
        MyThread myThread=new MyThread();
        //start方法,即为启动线程
        myThread.start();
    }

}
//创建一个自定义线程类MyThread ,继承jdk中的Thread类
class MyThread extends  Thread{
    /**
     * 重写Thread类中的run方法(Thread类实现了Runnable接口中的run方法)
     * run方法即为该线程启动后的线程任务
     */
    @Override
    public void run() {

            System.out.println("正在执行自定义线程类中的run方法");

    }
}

执行结果:

我们来解释一下代码,我们在idea中创建的线程类并不等于系统中的线程,java代码->在JVM中编译->调用系统提供的应用程序接口api(JVM已经将各种操作系统提供的api接口封装好了,为程序员提供了一个统一的接口)->系统创建一个线程TCB。    Java的Thread类通过JVM与操作系统的线程管理接口进行交互,Thread方便地地创建管理线程,无需直接操作底层的操作系统接口。通俗来说Thread类就是系统中线程TCB的一个抽象

java代码线程类并不等于系统中的线程,当线程类被实例化一个对象后,系统还被未创建线程,只有调用start方法后,操作系统才会创建一个线程TCB,去参与cpu调度,执行线程任务(run方法中定义的就是线程任务)

以上多个线程(main方法本事也是一个线程)的现象并不明显,看以下代码,做一些修改:

public class Main {
    public static void main(String[] args) {
        //用我们创建的线程类,new一个对象出来,此时相当于java层面的线程被创建但并未启动
        MyThread myThread = new MyThread();
        //start方法,即为启动线程
        myThread.start();
        //采用死循环的方式来让主线程持续的在执行,更好看现象
        while (true) {
            //使用sleep来限制一下打印速度,这里先不讨论sleep用法,只是用它限制输出速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("执行主线程中的任务");
        }
    }

}
//创建一个自定义线程类MyThread ,继承jdk中的Thread类
class MyThread extends  Thread{
    /**
     * 重写Thread类中的run方法(Thread类实现了Runnable接口中的run方法)
     * run方法即为该线程启动后的线程任务
     */
    @Override
    public void run() {
        //采用死循环的方式来线程持续的在执行,更好看现象
        while (true) {
            //使用sleep来限制一下打印速度,这里先不讨论sleep用法,只是用它限制输出速度
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("正在执行自定义线程类中的run方法");
        }
    }
}

我们定义了两个死循环循环,main方法中和MyThread类中的run方法,以往的经验因为mythread调用的start方法,启动了线程,执行任务,是一个死循环,那main方法中的while死循环就不会执行吗?     

当然会执行这是一个多线程来看结果:

不停的重复输出

上面的现象就明显发现多线程与单一线程编程的区别.

六.一些相关问题

到这里我们就有一些疑问(也许也没有也可以看看):

1.我们知道线程不是包含在进程里面吗?那么我们创建的线程外面的进程呢?

     线程当然是包含在进程中,当JVM运行时候,在系统中会创建一个JVM进程,当然我们所创建的线程包含在了这JVM进程中

2.我们为什么调用的是start方法而运行的确实run方法中的代码呢?

  我们可以看看start方法中的源码,进入后发现start方法,其实调用的是start0方法,而start0方法,

则是由native修饰方法修饰的即是调用本地代码,这里是系统提供的api

run方法与start方法的区别:

1.start方法是真实的像系统申请创建一个TCB而去参与cpu调度

2.run方法定义的是线程启动后中要执行的线程任务,如果像一个普通方法一样,被对象调用.run(),与普通方法没有什么区别。

还有一个现象是:同样的代码但是出现的结果并不是以上结果,由于我的电脑不会出现那种情况我用图片组合出那种情况:

对比一下发现了区别,这种情况下他们并不是同时执行的,好像有时候这个快一点有时候那个快一点这是为什么呢?

首先,这仍是多线程,不然怎么会同时执行两个死循环呢,出现这种现象的是由于cpu调度的问题导致的,cpu调度是根据操作系统自己定义的一套规则我们不好干预,cpu当然会尝试将多线程分散到不同的核心上,但是有时候也会根据优化资源,将多线程分配到一个核心上,那么就会出现‘线程抢占式’执行,那么就会出现一会这么快一点一会那个快一点,所以当前是哪个线程正在占用cpu是无法确定的

七.如何使用jconsole工具查看线程

使用jdk目录下\bin\jconsole.exe文件双击运行

或者使用命令行窗口输入jconsole

这是jdk安装后,自带的一个检测线程运行的工具运行起来后

进入此页面,当然我们先要让我们的线程持续运行起来,即写个死循环

选择我们的线程,点击连接

选择不安全的连接

点击线程就可以查看我们的线程Thread-0默认是从0开始,再运行一个线程即为Thread-1

可以点击查看,相关线程的线程任务,Thread-0中就是我们定义的run方法,Main中就是main方法

注:如果打开jconsole没有任何显示试一下使用管理员运行

八.创建线程的其他方法以其优化

1) 创建Runnable接口实现类定义线程任务

public class Main {
    public static void main(String[] args) {
        //创建线程任务对象
        MyRunnable myRunnable=new MyRunnable();
        //创建线程类,并将任务传给thread
        Thread thread1=new Thread(myRunnable);
        Thread thread2=new Thread(myRunnable);
        //启动线程
        thread1.start();
        thread2.start();
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Main方法正在执行");
        }
    }
}
//这时候定义的是线程的执行任务,并不是定义线程类
class MyRunnable implements Runnable{
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Runnable中的run方法正在执行");
        }
    }
}

Runnable接口是一个函数式接口(即内部只有一个run方法),我们自定义执行任务类,创建一个对象,将他传给Thread类对象中,启动线程就可以执行线程任务,让我们看看Thread类中的一些源码

那么使用定义线程任务的方式而不是使用定义线程类的方式的好处是什么,我们可以将线程类和线程任务分开,实现“高内聚低耦合”(意思是,让单一的模块只专注于做自己的事情,让模块与模块之间的依赖性减少),这样子对于后续对于代码的修改时候,所要做的工作就会减少很多,例如我们定义了一百个线程类,他们的任务是相同的,那么我们用定义线程类的方式,就要定义100个类,其中写100个相同的任务,再new上100个对象。那么我们用定义线程任务类的方式,只需要定义1个任务类,然后new上100个对象将任务类传给他们,是不是明显代码的简洁了许多,所以我们更加推介使用定义线程任务的方式创建线程

2)对于自定义线程类和自定义线程任务类两种创建线程方式的一些简写方式

 (1)使用匿名类简化

public class Main {
    public static void main(String[] args) {
        //使用匿名类定义thread类
        Thread thread=new Thread(){
            @Override
            public void run() {
                while(true){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("匿名类中的run方法正在执行");
                }
            }
        };
        //启动线程
        thread.start();
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Main方法正在执行");
        }
    }
}
new Thread{...}是一个匿名类的写法,定义的是Thread的子类在{}中可以重写父类的方法以及添加一些属性和方法(不过因为最后是赋值给父类,父类引用无法调用子类中特有的方法和属性,所以我们一般也只是重写方法)此时我们通过了匿名内部类的方法不用去自定义线程类,此时引用也就变成了Thread类了

还有其他优化方式:

public class Main {
    public static void main(String[] args) {
        //使用匿名内部类创建Runnable的实现类
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("匿名runnable实现类中的run方法正在执行");
                }
            }
        }
        );
        thread.start();
        while (true){
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Main方法正在执行");
            }
        }
    }
}

此时new Runnable{....}定义了一个匿名类,是Runnable接口的实现类(子类),返回的也是实现类(子类),使用匿名内部类就完成了对创建线程任务类的优化

(2)使用lambda表达式对匿名内部类写法的优化

Lambda表达式主要用于简化函数式接口的实现,所以我们只用他对创建线程任务的匿名内部类优化,因为其内部只有一个抽象方法

public class Main {
    public static void main(String[] args) {
        //lambda表达式写法()->{}
        Thread thread=new Thread(()->{
            while (true) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Runnable匿名内部类正在执行run方法,采用lambda表达式方式");
            }
        });
        //启动线程
        thread.start();
        while (true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Main方法正在执行");
        }

    }
}

此处就不再赘述lambda表达式的用法细节了。

总结:

看到这里想必你已经了解了进程与线程的关系(区别),以及如何使用java创建一个线程。以上就是本章的全部内容,感谢你的阅读。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值