初识线程~

今天给大家分享一下关于线程的知识点,主要就是线程是什么以及如何去创建一个线程,快来看看吧~

上一篇博客 初识进程~ 讲了进程的相关知识,那先回顾一下,为啥要有多个进程呢?就是为了实现并发编程,为什么要实现并发编程呢,就是CPU单个核心已经被用到最大化了,要想提升效率,就得使用多个核心.引入并发编程,就是为了充分利用好CPU的多核资源.


1.引入线程

使用多进程这种编程模式,是完全可以做到并发编程,也能够使CPU多核被充分利用.但是在某些场景下,如果需要频繁的创建或销毁进程,这个时候就会比较低效了.比如,你写了一个服务器程序,服务器要同一时刻给很多客户端提供服务的,这个时候就需要用到并发编程了,最典型的方法就是我这个服务器给每一个客户端都提供一个进程,实现一对一服务.有的客户端不一会就执行完了,这时就又要销毁进程了,这样一来就要频繁的创建进程和撤销进程.创建/撤销进程本身就是一个比较低效的操作.比如创建一个进程时,需要创建PCB, 分配系统资源(尤其是内存资源)(分配资源很占时间), 把PCB加入到内核的双向链表中等,这样的话就会使整个系统很低效.

为了提高这个场景下的效率,就有了"线程"(Thread),线程其实也叫做"轻量级进程",一个进程里面有很多个线程,每个线程也有自己的PCB,这样一来,一个进程里面可能就对应着多个PCB了.同一个进程里的多个线程之间,共同用一份系统资源.这就意味着,在一个进程里,新创建的线程,不必重新给它分配系统资源,只需要复用之前的就行了.所以创建线程就只需要创建PCB,把PCB加入到操作系统内核的链表中就行.少了分配系统资源这个步骤的话,就可以节约很多时间了,因为申请资源和释放资源是很占用时间的.这就是线程更轻量的原因.


2.进程和线程的区别

1).进程时包含线程的,线程是在进程内部的,一个进程至少包含一个线程

2).每个进程都有自己独立的虚拟地址空间,也有自己独立的文件描述符表,也就是说进程之间的资源是独立的,这也体现了进程的独立性;而在同一个线程里的多个线程之间,是共用这一份虚拟地址空间和文件描述符表的,也就是说在同一个进程中,线程之间的系统资源是共享的.

3).多个进程同时执行时,如果其中一个进程挂了,一般不会影响其他进程;而对在同一个进程里的多个线程而言,如果其中的某一个线程挂了,那么很有可能会把整个进程带走,这个进程中的其他线程也就没了.

4).进程是操作系统中资源分配的基本单位,而线程是操作系统中调度执行的基本单位.

5).讨论多进程的时候通常会谈到"父进程"/"子进程"(进程A里创建了进程B,则进程A就是进程B的父进程,进程B是进程A的子进程;但是在多线程里,就没有"父线程"/"子线程"这一说法,认为线程之间的地位是对等的.


3.创建线程代码样例

就拿我们初学java时写的第一个程序而言:

即使是这样一个最简单的输出语句,其实在运行的时候就涉及到线程了.因为运行这个程序时,操作系统会创建一个java进程,在这个java进程里就有一个线程(主线程)调用main方法.

虽然在上述代码中,我并没有手动的创建其他线程,但是java进程在运行的时候,内部也会创建出多个线程.

3.1 手动创建线程分析

说到创建一个线程,就不得不提到一个类:Thread.

比较原始的创建一个线程的做法,就是自己写一个类,让它继承Thread类,然后重写里面的run方法(run方法怎么实现就是看我们需要这个线程干的什么事了).把这个类写完后并不是就创建好线程了哈,这只是做好了准备工作,就比如公司需要招聘员工,自己完成这个类的设计就好像公司仅仅只发布了一个招聘信息;接下来你还要实例出一个对象,利用这个对象才能帮你去干活呀.new对象这个步骤就好比是员工招到了;以上步骤完成之后也并不是就创建了一个线程,你要再调用一个start方法,这时才开始真正创建线程.这个时候员工才开始干活.这时在操作系统内核中,创建出对应线程的PCB,然后将这个PCB加入到系统链表中去,参与调度.

以下是多线程的一个简单代码样例:

class MyThread extends Thread{
    //你需要这个run方法干什么活,你就怎么重写这个方法
    @Override
    public void run() {
        System.out.println("helle thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        //创建一个线程
        //写一个类,让它去继承Thread类,重写其中的run方法
        Thread t=new MyThread();
        t.start();
        System.out.println("hello main");
    }
}

分析:

小插曲:这里我想问一下大家,有没有注意到我启动thread这个线程时,用的是t.start(),为什么不是不是t.run()呢? run和start的区别是什么呢?这时一个经典的面试题~

使用start时,可以实现两个线程并发的执行;但是使用run时,其实就是直接调用run方法了,并没有创建新的线程,只是在之前的线程中,执行了run里面的内容.

使用start,则是创建新的线程,新的线程里面会调用run方法,新线程和main线程之间是并发执行的关系.

运行结果:

 

结果分析:看到这个结果,可能又有人想问了,按照我们原来的思想,代码从上往下执行,应该是先打印出"hello thread",后打印出"hello main",怎么却是反过来的呢?在这里,我们要知道:

1).每个线程是独立的执行流.main对应的是一个执行流,MyThread对应的是一个执行流,两个执行流之间是并发(并发+并行)的执行关系的. 

2).这两个线程执行的先后顺序,取决于操作系统的调度器的具体实现.这个调度规则我们程序员可以理解为"随机调度".因此在运行的时候,我们并不知道到底是先打印hello main 还是先打印hello thread,它的顺序我们是确定不了的,可能再运行一次顺序就变了,也可能运行100次,结果还是那样.当前看到的是先打印的main,大概率是受到创建线程自身的开销影响的.

3).因此,我们在编写多线程代码的时候,一定要注意到多个线程的执行顺序是随机/无序的.当然我们是有一些方法可以影响到线程的执行先后顺序的,但调度器自身的行为我们修改不了.调度器仍然是随机调度,咱们最多是让某个线程先等待,等待另一个线程执行完了自己在执行等.

3.2 创建线程的几种常见方法

1). 创建一个类继承Thread,重写run,最后start.(就是上面用来举例子的代码)

2).创建一个类实现Runnable接口,重写run.最后start.

 

这种写法,是把线程和任务分开了的,第一种写法,是把线程和任务写在一起的.很显然下面这种方法对比上一种能更好的解耦合.

3). 仍然是使用继承Thread类,但是不再显示的继承,而是使用"匿名内部类".

  • 匿名内部类创建Thread子类对象:
public class Demo3 {
    public static void main(String[] args) {
        //匿名内部类.这个匿名内部类没有名字,是Thread的子类.
        // 同时new这个关键字,就给这个匿名内部类创建了一个实例.
        // 这一整个操作就完成了继承,方法重写和实例化这三个步骤
        Thread t=new Thread(){
          public void run(){
              while(true) {
                  System.out.println("hello thread");
                  try {
                      Thread.sleep(1000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
        };
        t.start();
    }
}
  •  匿名内部类创建Runnable子类对象,再将其传给Thread对象:
public class Demo4 {
    public static void main(String[] args) {
        Runnable runnable=new Runnable() {
            @Override
            public void run() {
                while(true){
                    System.out.println("hello thread");
                }
            }
        };
        Thread t=new Thread(runnable);
        t.start();
    }
}

以上这种写法还可以直接写成这样:

 

  • 使用lambda表达式创建Runnable子类对象:

使用lambda表达式,,是更简单的写法,也是经常使用的一种写法,比较推荐.形容lambda表达式这样的,能够简化代码编写的语法规则的,称为"语法糖".这种语法并不是不可 替代的,只是一种让代码更简洁的写法.

好啦,线程的创建方式就先介绍到这里啦,但并不是说只有这几种哦,据我所知,也还可以基于Callable/Future Task的方式创建,也还可以基于线程池的方式创建等.


4.线程的优点

  1. 创建一个线程的代价比创建一个进程要小的多.
  2. 与进程之间的切换相比,线程之间的切换对操作系统而言更简单.
  3. 线程占用的资源要比进程少很多.
  4. 可以更充分的利用多CPU的可并行数量.
  5. 在等在I/O处理的同时,程序可执行其他的计算任务.
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程去实现.
  7. I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作.

 好了,今天的共享知识就到这里了,本人知识水平有限,写的不妥的地方还请各位多多指正,评论私信都行,咱们下次再见~~看完的铁子们就给个赞吧~

 

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哆啦A梦的110

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值