线程的引入
上一篇中,我们主要讲到了进程,多任务操作系统,希望系统能同时运行多个程序.如果是单任务的操作系统,完全不涉及进程,也不需要管理,更不需要调度.因此,本质上来说,进程是用来解决"并发编程"这样的问题的.但在一些特定的情况下,进程的表现,往往不尽如人意.比如,有些场景下,需要频繁的创建和销毁进程的时候,此时使用多进程编程,系统的开销就会很大.
对于服务器出现的问题
而对于服务器来说,它同一时刻会收到很多请求,而针对每个请求,都会创建出一个进程,给这个请求提供一定的服务,返回对应的响应.一旦这个请求处理完了,此时这个进程就要销毁了.如果请求很多,意味着服务器就需要不停地创建新的进程,也不停销毁旧的进程.频繁地进行创建和释放,这样的操作,开销是比较大的.其中最关键的原因,就在于资源的申请与释放.因为进程是资源(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"); });