线程的认识

简介: 线程的认识

线程的引入

上一篇中,我们主要讲到了进程,多任务操作系统,希望系统能同时运行多个程序.如果是单任务的操作系统,完全不涉及进程,也不需要管理,更不需要调度.因此,本质上来说,进程是用来解决"并发编程"这样的问题的.但在一些特定的情况下,进程的表现,往往不尽如人意.比如,有些场景下,需要频繁的创建和销毁进程的时候,此时使用多进程编程,系统的开销就会很大.

对于服务器出现的问题

对于服务器来说,它同一时刻会收到很多请求,而针对每个请求,都会创建出一个进程,给这个请求提供一定的服务,返回对应的响应.一旦这个请求处理完了,此时这个进程就要销毁了.如果请求很多,意味着服务器就需要不停地创建新的进程,也不停销毁旧的进程.频繁地进行创建和释放,这样的操作,开销是比较大的.其中最关键的原因,就在于资源的申请与释放.因为进程是资源(CPU,硬盘,内存,网络带宽)分配的基本单位.

一个进程,刚刚启动的时候,首当其冲,就是内存资源.进程需要把依赖的代码和数据,从磁盘加载到内存中.而从系统分配一个内存,并非是一件容易的事情.一般来说,申请内存的时候,需要指定一个大小.系统内部就把各种大小的空闲内存,通过一定的数据结构,给组织起来了.实际申请的时候,就需要去这样的空间进行查找,找到大小合适的内存,分配过来.

结论:进程在进行创建和销毁时,开销大(主要体现在资源的申请与释放上).

线程,就是上述问题的解决方案.线程也可以称为一种轻量级的进程,在进程的基础上,做出了改进.下面就让我们走进线程,来看一看线程是如何解决这样的问题吧.

认识线程

概念

(1)线程是什么

一个线程就是一个执行流.每个线程之间都可以按照顺序执行自己的代码.多个线程之间"同时"执行多份代码.通俗地来讲,就是可以使一个代码的多处部分,同时进行.

因此,我们说线程保持了独立调度执行,这样的"并发支持",同时省去了"分配资源""释放资源"带来的额外开销.

(2)为啥要有线程

首先,"并发编程"成为"刚需"

单核CPU的发展遇到了瓶颈,要想提高算力,就需要多核CPU.而并发编程能更充分利用多核CPU资源.

有些任务场景需要"等待IO",为了让等待IO的时间能够去做一些其它的工作,也需要用到并发编程.

其次,虽然多进程也能实现并发编程,但是线程比进程更轻量.

创建线程比创建进程更快.

销毁线程比销毁进程更快.

调度线程比调度进程更快.

前面介绍了会使用PCB来描述一个进程.现在,也使用PCB来描述一个线程.来看下面这张图:

先来说一个概念:进程是包含线程的,.

在图一中,是一个进程有一个PCB,但是实际上,一个进程可以有多个PCB,意味着这个线程包含了一个线程组(多个线程).操作系统,进行"多任务调度",本质上是在调度PCB(线程在系统中的调度规则,就和之前的进程是一样的),线程中的PCB中也有状态,优先级,上下文,记账信息.

PCB中,有个属性,是内存指针.多个线程的PCB的内存指针,指向的是同一个内存空间.这样就意味着,只是创建第一个线程的时候需要从系统分配资源.后续的线程,就不必分配.直接共用前面的那份资源就可以了.除了内存以外,文件操作符表(操作硬盘)这个也是多个线程共用一份的.

(3)进程与线程的区别

1.进程是包含线程的.每个进程至少有一个线程存在,即主线程

2.每个线程也是独立的执行流,可以执行一些代码,并单独参与到CPU调度中(状态,上下文,优先级,记账信息)每个线程都有一份.

3.进程和进程之间不共享资源,同一个进程和线程之间共享一个内存空间(文件操作符表)

4.根据2,3可得:进程是资源分配的基本单位,线程是调度的基本单位.

5.进程与进程间,不会相互影响,如果某个进程中某个线程抛出异常,可能会影响到其它线程

6.同一个进程中的线程间,可能会互相干扰,引起线程安全问题.

7.线程也不是越多越好,要足够合适,如果线程太多了,调度开销可能会十分明显

(4)Java线程和操作系统中的线程之间的关系

线程是操作系统中的概念,操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(如Linux的pthread库)

Java标准库中Thread类可以视为是对操作系统提供api进行了进一步的抽象与封装.

第一个多线程程序

感受多线程程序和普通程序的区别:

每个线程都是一个独立的执行流,多个线程之间是"并发"执行的.

import static java.lang.Thread.sleep;
 
class MyThread extends Thread {
    @Override
    //run方法是线程的入口方法(不需要程序员手动调用,会在合适时机(线程创建好之后),被jvm自动调用执行)
    public void run() {
        while(true) {
            System.out.println("run方法中的线程执行....");
 
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        //start()方法用于启动线程
        t.start();
 
        while(true) {
            System.out.println("main方法中的线程执行....");
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

通过打印的方式可以清晰地看到两个执行流的执行逻辑(线程的执行逻辑),那么有什么方法能更清晰地看到两个线程的执行呢?这就用到了jdk中的jconsole工具.

在你已经运行了程序之后,可以进来看到线程的情况

创建线程

方法1:继承Thread类

继承Thread来创建一个线程类.

//因为Thread是在java.lang路径下的,因此不用导包
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("这里是线程运行的代码");
    }
}

创建MyThread类的实例

MyThread t = new MyThread();

调动start方法启动线程(本质上是会调用系统的api,来完成创建线程操作)

t.start();  //线程开始运行

方法2:实现Runnable接口

1.实现Runnable接口

/*
Runnable可理解成"可执行的",通过该接口,
就可抽象表示出一段可被其它实体来执行的代码
*/
class MyRunnable implements Runnable {
    //runnable要表示的这段代码
    @Override
    public void run(){
        System.out.println("这里是线程执行的代码");
    }
}

2.创建Thread类实例,调用Thread的构造方法时将Runnable对象作为target参数.

//搭配Thread类以创建真正的线程
Thread t = new Thread(new MyRunnable());

3.调用start方法

t.start();    //线程开始运行

对比上面的两种方法:

1.继承Thread类,直接使用this就可以表示当前线程对象的引用.

2.实现Runnable接口,this表示的是MyRunnable的引用.需要使用Thread.currentThread()

(后面会介绍)

方法3:继承Thread,重写run,但是使用匿名内部类

//使用匿名内部类创建Thread子类对象
Thread t1 = new Thread() {
    @Override
    public void run() {
        System.out.println("使用匿名类创建Thread子类对象");
    }
}

简要回顾一下匿名内部类这个知识点:匿名内部类是内部类,即在一个类里面定义的类.由于它是匿名的,所以就意味着不能重复使用,用完一次就扔了.

在Thread t1 = new Thread() {...};中,写{}的意思是要定义一个新的类.与此同时,这个新的类,继承自Thread.此处{}中可以定义子类的属性和方法.此处最重要的目的就是重写run方法.

于此同时,这个代码还创建了子类的实例.因此,t1指向的实例,并非是单纯的Thread,而是Thread的子类(但是我们不知道这个子类叫啥,因为它是匿名的).

方法4:匿名内部类创建Runnable子类对象

//使用匿名内部类创建Runnable子类对象
Thread t2 = new Thread(new Runnable() {
    //Thread构造方法的参数,填写了Runnable的匿名内部类实例
    @Override 
    public void run() {
        System.out.println("使用匿名类创建Runnable子类对象");
    }
});

方法五:lambda表达式创建Runnable子类对象(常用/推荐)

//这里()是形参列表,这里能带参数,但线程的入口不需要参数
//比如使用lambda代替Comparator,可以带上两个参数
//()前面应该有一个函数名,但此处作为匿名函数,就没有名字了
Thread t = new Thread(() -> {
    System.out.println("hello thread");
});
相关文章
|
3月前
|
存储 安全 Java
C++线程浅谈
C++线程浅谈
|
5月前
|
C#
C#线程初步
C#线程初步
20 0
|
10月前
|
算法 Java
线程通过管道通信
线程通过管道通信
|
11月前
|
Java Linux 调度
03.关于线程你必须知道的8个问题(中)
大家好,我是王有志,欢迎来到《Java面试都问啥?》。我们书接上回,继续聊Java面试中关于线程的问题。
57 1
03.关于线程你必须知道的8个问题(中)
|
11月前
|
Java
线程理解
个人学习理解
59 0
|
11月前
|
传感器 存储 自动驾驶
(6)线程
(6)线程
72 0
|
Java 调度
线程小记
线程小记
|
Java
什么是线程
什么是线程
107 0
线程睡眠
Thread.sleep方法会导致当前线程暂停执行一段指定的时间...
|
存储 Linux
线程局部存储
TLS:Thread Local Storage,线程局部存储声明为TLS的变量在每个线程都会有一个副本,各个副本完全独立,每个副本的生命期与线程的生命期一样,即线程创建时创建,线程销毁时销毁。 C++11起可以使用thread_local关键字声明TLS变量,变量可以是任意类型。
2015 0