0
点赞
收藏
分享

微信扫一扫

进程、线程的创建方法及 Thread 类的基本用法

自由的美人鱼 2022-04-01 阅读 100

目录


CPU:本质上是通过一些基础的门电路构成的:
电子开关=>基础门电路=>半加器=>全加器=>加法器=>进行加减乘除
CPU跑的快主要是因为当前这个CPU集成程度非常高.里面包含了非常多的门电路。


一、进程

操作系统是一个管理的软件。
对下,要管理好各种硬件设备
对上,操作系统要给各种软件提供稳定的运行环境
操作系统中提供的功能,是非常复杂的,这里重点介绍操作系统中针对"进程"这个软件资源的管理。

进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程;同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位
进程(process)又叫任务(task)。

1.1 操作系统是如何管理进程的?(进程是如何管理的)

  1. 先描述一个进程(明确出一个进程上的一些相关属性)

操作系统里面主要都是通过C/C++来实现的.此处的描述其实就是用的C语言中的"结构体"(也就和Java的类差不多)。操作系统中描述进程的这个结构体,称为"PCB" (process control block 进程控制块)。

  1. 再组织若干个进程(使用一些数据结构,把很多描述进程的信息放在一起,方便进行增删改查)

典型的实现就是使用一个双向链表,把每个进程的PCB给串起来。操作系统的种类是很多,内部的实现也是各有不同,此处所讨论的情况是以Linux这个系统为例。

所谓的“创建进程",就是先创建出PCB,然后把PCB加到双向链表中;
所谓的"销毁进程",就是找到链表上的PCB,并且从链表上删除;
所谓的“查看任务管理器"就是遍历链表。

1.2 PCB(进程控制块)中的一些属性

  1. pid(进程id):进程的身份标识—进程的身份证号;

  2. 内存指针:指明了进程要执行的指令/代码在内存哪里,以及进程中依赖的数据都在哪里。当运行一个exe,此时操作系统就会把这个exe加载到内存中,变成进程.

  3. 文件描述符表:程序运行过程中经常要和文件打交道(文件是在硬盘上),进程每次打开一个文件,就会在文件描述符表上多增加一项(这个文件描述符表就可以视为一个数组,里面的每个元素又是一个结构体,就对应一个文件的相关信息)。
    一个进程只要一启动,不管代码中是否写了打开/操作文件的代码,都会默认的打开三个文件(系统自动打开的):标准输入(System.in),标准输出(System.out),标准错误(System.err);这个文件描述符表的下标,就称为文件描述符。

1.3 进程调度的基本过程

上面的属性是一些基础的属性.下面的一组属性,主要是为了能够实现进程调度

状态:这个状态就描述了当前这个进程接下来应该怎么调度
就绪状态:随时可以去CPU上执行.
阻塞状态/睡眠状态:暂时不可以去CPU上执行.(Linux中的进程状态还有很多其他的…)

优先级:先给谁分配时间,后给谁分配时间,以及给谁分的多,给谁分的少

记账信息:统计每个进程,都分别被执行了多久,分别都执行了哪些指令,分别都排队等了多久;给进程调度提供指导依据的。

上下文:表示了上次进程被调度出CPU的时候,当时程序的执行状态,下次进程上CPU的时候,就可以恢复之前的状态,然后继续往下执行。
进程被调度出CPU之前,要先把CPU中的所有的寄存器中的数据都给保存到内存中(PCB的上下文字段中)相当于存档了.下次进程再被调度上CPU的时候,就可以从刚才的内存中恢复这些数据到寄存器中.相当于读档了.
存档+读档~~ 存档存储的游戏信息,就称为"上下文"。

1.4 进程间通信

进程的调度,其实就是操作系统在考虑CPU资源如何给各个进程分配
内存资源又是如何分配的呢?通过虚拟地址空间
由于操作系统上,同时运行着很多个进程,如果某个进程,出现了bug,进程崩溃了,是否会影响到其他进程呢?现代的操作系统(windows, linux, mac…)能够做到这一点,就是“进程的独立性"来保证的,就依仗了"虚拟地址空间"。

进程之间现在通过虚拟地址空间,已经各自隔离开了,但是在实际工作中,进程之间有的时候还是需要相互交互的。这就是进程间通信

操作系统中提供了多种这样的进程间通信机制(有些机制是属于历史遗留的,已经不适合于现代的程序开发)
现在最主要使用的进程间通信方式,两种:
1.文件操作
2.网络操作(socket)

二、线程

1.线程是什么?

一个线程就是一个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码。
例如:一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。

2.为什么要有线程?

我们的系统支持多任务了,程序猿也就需要"并发编程",通过多进程,是完全可以实现并发编程的,但是如果需要频繁的创建/销毁进程,这个事情成本是比较高的.如果需要频繁的调度进程,这个事情成本也是比较高的
解决上述这个问题的方法:

1.进程池(数据库连接池.字符串常量池)
进程池虽然能解决上述问题,提高效率,同时也有问题.池子里的闲置进程,不使用的时候也在消耗系统资源.消耗的系统资源太多。

2.使用线程来实现并发编程,线程比进程更轻量.每个进程可以执行一个任务.每个线程也能执行一个任务(执行一段代码),也能够并发编程.
创建线程比创建进程更快.
销毁线程比销毁进程更快.
调度线程比调度进程更快.

3. 为啥线程比进程更轻量?(进程重量是重在哪里?)

重在资源申请释放(在仓库里找东西…)
线程是包含在进程中的.一个进程中的多个线程,共用同一份资源(同一份内存+文件)
只是创建进程的第一个线程的时候(由于要分配资源)成本是相对高的,后续这个进程中再创建其他线程,这个时候成本都是要更低一些,不必再分配资源。

4. (重点)线程与进程的区别?

  1. 进程包含线程,一个进程里面可以有一个线程,也可以有多个线程
  2. 进程和线程都是为了处理并发编程这样的场景,但是进程在频繁创建和销毁中,开销更高.线程开销更低.(线程比进程更轻量)
  3. 进程是系统分配资源(内存,文件资源…)基本单位.线程是系统调度执行的基本单位(CPU)。
  4. 进程具有独立性,每个进程有各自独立的独立的虚拟地址空间,一个进程挂了,不会影响其他进程。同一个进程有多个线程,共用同一块内存空间,一个线程挂了,可能影响到其他线程。

5. Java 的线程和操作系统线程的关系

线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.

线程之间是并发执行的

在一个进程中,至少会有一个线程.在一个java进程中,也是至少会有一个调用main方法的线程(这个线程不是你手动搞出来的)
如下面例子中,自己创建的thread 线程和自动创建的main线程,就是并发执行的关系(宏观上看起来是同时执行)。

class MyThread2 extends Thread{
    @Override
    public void run() {
        //run方法中得逻辑,是在新创建出来的线程中被执行的代码
        while (true){
            System.out.println("hello thread");
            try {
                //这个休眠操作,就是强制的让线程进入阻塞状态,单位是ms 1s之内这个线程不会到cpu上执行
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Test02 {
    public static void main(String[] args) {
        Thread thread = new MyThread2();
        thread.start();

        while (true){
            //一个进程中,至少会有一个线程
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

执行结果:
在这里插入图片描述

现在两个线程,都是打印一条,就休眠个1s,每一轮1s时间到了之后,到底是先唤醒main还是 thread,这是不确定的(随机的)。
对操作系统来说,内部对于线程之间的调度顺序,在宏观上可以认为是随机的(抢占式执行)。这个随机性,会给多线程编程带来很多麻烦。

6. 创建线程的5种写法

方法1 继承 Thread 类

创建子类,继承自Thread,并且重写run方法.

run方法中描述了这个线程内部要执行哪些代码.每个线程都是并发执行的. (各自执行各自的代码)因此就需要告知这个线程,你执行的代码是啥。并不是我一定义这个类,一写run方法,线程就创建出来,需要调用start方法,才是真正的在系统中创建了线程,才是真正开始执行上面的run操作。在调用start之前,系统中是没有创建出线程的。

class MyThread extends Thread{
    @Override
    public void run() {
        //run方法中得逻辑,是在新创建出来的线程中被执行的代码
        System.out.println("hello");
    }
}
public class Test01 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        //执行到这里,才真正在系统中创建了线程
        t.start();
    }
}

方法2 实现 Runnable 接口

创建一个类,实现Runnable接口,再创建Runnable实例传给Thread实例。

//通过Runnable来描述任务的内容,然后进一步再把描述好的任务交给Thread
class MyRunnable implements Runnable{
    @Override
    public void run() {     
        System.out.println("hello");
    }
}
public class Test03 {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

方法3/4:使用匿名内部类

创建了一个匿名内部类,继承自Thread类.同时重写run方法.同时再new出这个匿名内部类的实例。并调用satrt来开启线程。

public class Test04 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
                System.out.println("hello run");//hello run
            }
        };
 		  t.start();
    }
}

new的 Runnable,针对这个创建的匿名内部类.同时new 出的Runnable 实例传给Thread 的构造方法。

public class Test05 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello runnable");
            }
        });
        t.start();
    }
}

通常认为Runnable这种写法更好一点,能够做到让线程和线程执行的任务,更好的进行解耦。

方法5:使用lambda表达式

使用lambda代替了Runnable

public class Test04 {
    public static void main(String[] args) {
        //方法5 使用lambda代替Runnable
        Thread t = new Thread(()->{
            System.out.println("hello thread");
        });
        t.start();
    }
}

7. 多线程的优势-提高任务完成的效率

例如:有两个整数变量,分别要对这俩变量自增10亿次,分别使用一个线程,和两个线程。
可以观察多线程在一些场合下是可以提高程序的整体运行效率的

串行执行

public class Test06 {
    //在写一个比较长的整数常量的时候,可以通过下划线来进行分隔
    private  static  final long count = 10_0000_0000;
    //serial 串行执行
    public static void serial(){
        long a = 0;
        long b = 0;
        long begin = System.currentTimeMillis();
        for (long i = 0; i < count; i++) {
            a++;
        }
        for (long  i = 0; i < count; i++) {
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("消耗时间:" +(end - begin) + "ms");
    }

public static void main(String[] args) throws InterruptedException {
        serial();
    }
}

执行结果:
在这里插入图片描述

并行执行

public class Test06 {
    //在写一个比较长的整数常量的时候,可以通过下划线来进行分隔
    private  static  final long count = 10_0000_0000;
	//concurrency 并发性
    public static void concurrency() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread t1 = new Thread(()->{
            long a = 0;
            for (long i = 0; i < count; i++) {
                a++;
            }
        });
        t1.start();

        Thread t2 = new Thread(()->{
            long b = 0;
            for (long i = 0; i < count; i++) {
                b++;
            }
        });
        t2.start();
        
        //让main线程等待t1
        t1.join();
        //让main线程等待t2
        t2.join();
        long end = System.currentTimeMillis();
        System.out.println("消耗时间:" +(end - begin) + "ms");
    }
    public static void main(String[] args) throws InterruptedException {
  	 concurrency();
    }
}

执行结果:
在这里插入图片描述
比较执行结果,可明显看出并行执行较串行执行效率显著提升。

三、Thread 类及常见方法

3.1 Thread 的常见构造方法

在这里插入图片描述
例:

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
//创建线程对象,并命名
Thread(String name) 

这个是给线程(thread 对象)起一个名字,起一个啥样的名字,不影响线程本身的执行,
仅仅只是影响到程序猿调试.可以借助一些工具看到每个线程以及名字.
它很容易在调试中对线程做出区分,可以使用jconsole来观察线程的名字。
在这里插入图片描述

3.2 Thread 的几个常见属性

在这里插入图片描述

//是否后台线程
 isDaemon()

如果线程是后台线程,就不影响进程退出.
如果线程不是后台线程(前台线程),就会影响到进程退出
如:创建的t1和t2默认都是前台的线程,即使main方法执行完毕,进程也不能退出,得等t1和t2都执行完,整个进程才能退出!!!
如果t1和t2是后台线程,此时如果main 执行完毕,整个进程就直接退出, t1和t2就被强行终止了。

//是否存活 
isAlive()

操作系统中对应的线程是否正在运行.
Thread t对象的生命周期和内核中对应的线程,生命周期并不完全一致。创建出t对象之后,在调用start之前,系统中是没有对应线程的,在run方法执行完了之后,系统中的线程就销毁了。但是t这个对象可能还存在,通过isAlive()就能判定当前系统的线程的运行情况。
如果调用start之后, run 执行完之前, isAlive就是返回true
如果调用start之前, run执行完之后, isAlive就返回false

例:

public class Test12 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ": 我还活着");
                            Thread.sleep(1 * 1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName() + ": 我即将死去");
        });
        System.out.println(Thread.currentThread().getName()
                + ": ID: " + thread.getId());
        System.out.println(Thread.currentThread().getName()
                + ": 名称: " + thread.getName());
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
        System.out.println(Thread.currentThread().getName()
                + ": 优先级: " + thread.getPriority());
        System.out.println(Thread.currentThread().getName()
                + ": 后台线程: " + thread.isDaemon());
        System.out.println(Thread.currentThread().getName()
                + ": 活着: " + thread.isAlive());
        System.out.println(Thread.currentThread().getName()
                + ": 被中断: " + thread.isInterrupted());
        thread.start();
        while (thread.isAlive()) {}
        System.out.println(Thread.currentThread().getName()
                + ": 状态: " + thread.getState());
    }
}

执行结果:
在这里插入图片描述

3.3 启动一个线程-start()

调用 start 方法, 才真的在操作系统的底层创建出一个线程.

start和run的区别

run 单纯的只是一个普通的方法,描述了任务的内容.
start则是一个特殊的方法,内部会在系统中创建线程.

3.4 中断一个线程

中断线程就是让一个线程停下来。
线程停下来的关键,是要让线程对应的run方法执行完(还有一个特殊的,是main这个线程.对于main来说,得是main方法执行完,线程就完了)。

目前常见的有以下两种方式:

1. 通过共享的标记来进行沟通

可以手动的设置一个标志位(自己创建的变量, boolean),来控制线程是否要执行结束。

public class Test07 {
    public static boolean isQuite = false;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while (!isQuite){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        isQuite = true;
        System.out.println("终止t线程");
    }
}

输出结果:
在这里插入图片描述

在其他线程中控制这个标志位,就能影响到这个线程的结束.
此处因为多个线程共用同一个虚拟地址空间。因此, main线程修改的isQuitt线程判定的isQuit,是同一个值。

2. 调用 interrupt() 方法来通知

使用 Thread.interrupted() (这是一个静态方法)
或者 Thread.currentThread().isInterrupted()(这是一个实例方法,其中currentThread能够获取到当前线程的实例) 代替自定义标志位.

推荐使用: Thread.currentThread().isInterrupted()这个方法。

public class Test08 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            //中断一个线程
           while (!Thread.currentThread().isInterrupted()){
               System.out.println("hello thread");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   e.printStackTrace();
                   //做一些收尾工作
                   System.out.println("收尾工作");
                   //当触发异常之后,立即退出循环
                   break;
               }
           }
        });
        t.start();

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
  
        //调用这个方法可能产生:
        //如果t线程是就绪状态,就是没有设置标志位为true
        //如果t线程处于阻塞状态(sleep休眠了),就会触发一个InterruptedException
        t.interrupt();
    }
}

调用这个interrupt方法,就会让sleep触发一个异常从而导致线程从阻塞状态被唤醒。当下的代码,一旦触发了异常之后,就进入了catch语句,在catch中,就单纯的只是打了一个日志,printStackTrace是打印当前出现异常位置的代码调用栈,打完日志之后,就直接继续运行。

运行结果:
在这里插入图片描述

3.5 等待一个线程-join()

多个线程之间,调度顺序是不确定的,线程之间的执行是按照调度器来安排的.这个过程可以视为是"无序,随机"这样不太好.有些时候,我们需要能够控制线程之间的顺序.
线程等待就是其中一种控制线程执行顺序的手段,此处的线程等待,主要是控制线程结束的先后顺序

public class Test09 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                System.out.println("hello thread");
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        
		//首先,调用这个方法的线程,是main 线程.针对t这个线程对象调用的.
		//此时就是让main等待t 
        t.join(1000);//指定最大等待时间
    }
}

执行结果:
在这里插入图片描述
调用join之后, main线程就会进入阻塞状态(暂时无法在cpu上执行)
代码执行到join这一行,就暂时停下了,不继续往下执行了,然后join 啥时候能继续往下走,恢复成就绪状态呢?就是等到t线程执行完毕(t的run方法跑完了)。
通过线程等待,就是在控制让t先结束, main后结束,一定程度上的干预了这两个线程的执行顺序。
在上述二的第7小节中就用到了join。

在这里插入图片描述

public void join(long millis) //等待线程结束,最多等 millis 毫秒

进入join 也会产生阻塞.这个阻塞不会一直持续下去.如果在最大等待时间之内, t线程结束了,此时join直接返回,如果在最大等待时间之后,t仍然不结束, 此时 join 也就直接返回。

3.6 获取当前线程实例的引用

//返回当前线程对象的引用
public static Thread currentThread(); 

例:

public class Test10 {
    public static void main(String[] args) {
        Thread t = new Thread(){
            @Override
            public void run() {
            //同样可以拿到当前Thread的实例
//       System.out.println(Thread.currentThread().getName());//Thread-0

                //拿到当前Thread的实例
                System.out.println(this.getName());//Thread-0
            }
        };
        t.start();

        //拿到main线程的实例
        System.out.println(Thread.currentThread().getName());//main
    }
}

Runnablelambda表达式都不能获得Thread的实例,Runnable只是一个单纯的任务,没有name属性,lambda同理。

3.7 休眠当前线程

因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间。

进程:PCB+双向链表;这个说法是针对只有一个线程的进程。
如果是一个进程有多个线程, 此时每个线程都有一个PCB,一个进程对应的就是一组PCB了。
PCB上有一个字段tgroupld,这个id其实就相当于进程的id.同一个进程中的若干个线程的tgroupld是相同的!!!

如果某个线程调用了sleep方法,这个PCB就会进入到阻塞队列,操作系统调度线程的时候,就只是从就绪队列中挑选合适的PCB到CPU上运行,阻塞队列里的PCB就只能干等着。当睡眠时间到了,系统就会把刚才这个PCB从阻塞队列挪回到就绪队列。
在前面的案例中大多都有用到sleep方法。


举报

相关推荐

0 条评论