1.进程
上一篇文章提到:进程是一个正在运行的程序。进程之间的资源是独立的。同一时刻一个逻辑核心可以运行一个进程。进程在操作系统中,用进程控制块(PCB)来描述进程。PCB中有很多属性来描述进程。并发编程是并发(分时复用)+并行。
在多任务操作系统中,需要运行多个程序,就需要实现并发编程。 进程可以很好的解决并发编程这个问题。有的操作系统只有进程这个概念。
但是有些情况下,进程就不是那么好用了。在早期的网站就是基于多进程进行编程的。网站是浏览器-服务器结构。用户通过浏览器,向服务器发送请求,服务器接收请求,并返回响应。如果同一时刻一个用户向服务器发送请求,服务器接收请求时会创建一个新的进程,返回响应后,会销毁这个进程。
如果同一时刻,有多个用户向服务器发送请求,那么服务器就要不断的创建和销毁,这对于系统的开销是比较大的。因为每次创建一个进程都要向操作系统申请分配该进程需要用到的资源,比如寄存器,内存,网络带宽,硬盘等等,每次销毁,就要释放掉这些资源。
一个进程刚启动的时候,第一步,就是分配内存资源,进程需要将数据和依赖的代码,从磁盘加入到内存里。从操作系统中,去分配内存并不是一件容易的事。操作系统需要去查找一个大小空闲的内存去分配给进程。
进程不断创建和销毁的开销比较大,开销大体现在,不断的申请和释放资源。
进程是资源分配的基本单位。在多核CPU中,进程可以在单个核心上分配资源,也可以在多个核心上分配资源,取决于具体的调度策略。
为了解决进程需要频繁创建和销毁的问题,引入了线程。
2.线程
线程是在进程的基础上进行改进。保持了独立调度执行,支持并发,省去分配资源和释放资源的额外开销。(ps:不是没有开销,线程通过复用资源的方式来提高了创建销毁的效率。)
我们用PCB来表示进程,我们也可以用PCB来表示线程。进程有的PCB属性,线程也有。比如PID,文件操作表符,上下文,状态等等。
进程先创建,从系统中申请一块内存,然后在该进程中创建的线程的内存空间和进程是同一个内存空间。和同一个进程共享资源的线程,就是该进程的线程组。
进程和该进程的线程组的,PID,内存指针,文件操作表符等和资源分配相关的属性是一样的。所以只需要对进程进行资源分配,随后创建的线程,就省去了资源分配和资源释放带来的额外开销。可以把线程理解为轻量级进程。
进程是系统分配资源的基本单位,线程是系统调度执行的基本单位。
但是没有线程前,进程即是系统分配资源的基本单位,也是系统执行调度的基本单位。所以引入线程后一些说法可以更新了。在同一时刻,一个逻辑核心上,只能运行一个线程。
进程至少包含一个线程,创建第一个线程的时候,进程也被创建了。
进程与进程之间资源独立,线程与线程之间资源共享。
进程申请系统分配资源并不是一次性的,在程序的运行中进程还能继续申请资源。
例子:坤坤搬运100个货物
一个工厂里一个坤坤来搬运100个货物,比较慢,老板非常的不满意。所以可以用进程,再搞一个工厂,分一半货物,让两个坤坤,同时搬货,速度就更快。
但是老板觉得还是太慢了,新建工厂很耗费钱,(新建进程很耗费系统资源),老板希望不要再新建工厂并且还要提高搬货速度。所以,可以用线程,每个工厂多安排几个坤坤(线程)来搬货。
但是每个工厂的坤坤(线程)不能无限增加,如果超过一定限度就不会有效率上的提升了,而且还会竞争CPU的资源,CPU的资源是有限的,会增加线程调度的开销。
多线程之间,不同的线程还有可能会起冲突,从而导致代码产生逻辑上的错误。这种就是线程安全问题。比如,两个坤坤,都想搬同一块货,狭路相逢,同时开搬,谁能搬到这是不确定的。
多线程共享资源,如果一个线程抛出异常,没有处理好,可能会让进程直接终止。比如,一个坤坤,搬的货突然破了,坤坤要赔钱了,坤坤生气了,坤坤于是决定让大家都来陪他一起赔钱,就拿篮球把货都砸了。
2.进程与线程的关系/区别
- 进程包含了线程,线程是进程的一部分,第一个线程创建的同时,进程也创建了。
- 进程是系统分配资源的基本单位,线程是系统调度的基本单位。
- 进程之间资源独立,进程里的线程之间共享资源。进程和进程之间不会互相影响,但是同一个进程的线程如果抛出异常,可能会导致这个进程的所有直接终止。线程之间可能会互相干扰,导致线程安全问题。
- 每个线程都是独立的执行流,有自己的代码和数据。
3.多线程编程
实现并发编程,可以使用多进程编程,也可以使用多线程编程。但是用java实现并发编程,java官方比较推荐使用多线程。在java标准库中,很多和多进程编程相关的api没有提供。
操作系统的作用:
- 管理计算机硬件资源,就是对各种硬件提供的api进行统一封装。
- 为上层软件的运行提供稳定的环境。
Java语言,号称一处编译,处处执行,这句话的基础就是机器上装有JVM(java虚拟机),java程序是跑在JVM上的,只要机器有JVM虚拟机,就可以直接跑java程序。
操作系统对硬件提供的api进行封装,然后操作系统提供api给程序员进行编程来操纵硬件资源,JDK(java开发工具包)对于操作系统原生api进行封装,然后提供api给java程序员进行编程。
3.1 java实现多线程的方式
java提供实现多线程编程的api是java.lang包下的Thread类。
- 继承Thread类,重写run()方法
//1. 继承Thread类
class Thread1 extends Thread {
//2. 重写run方法
@Override
public void run() {
System.out.println("hhh1");
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 3. 根据上面的类,创建出Thread实例
Thread t = new Thread1();
// 4.调用Thread中的start方法,此时才会去调用系统api,
//在内核中创建出线程,然后JVM才会调用run方法
t.start();
}
}
创建一个线程,需要实例化Thread类。
重写的run()方法,是创建的线程t的入口方法,被JVM所调用,不是由程序员自己手动调用,这种方法,被称为回调函数。重写方法的本质是对现有类进行一个扩展。进程中创建的第一个线程,称为主线程。 main()方法是主线程的入口方法。
Thread实例只有调用的start()方法,才会去调用系统api,真正在内核中创建出线程。没有调用start方法之前,不能算创建出真正的线程。
- 实现Runnable接口,重写run()方法
class Thread2 implements Runnable {
@Override
public void run() {
System.out.println("hhh2");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Runnable runnable = new Thread2();
Thread t = new Thread(runnable);
t.start();
}
}
Runnable runnable = new Thread2(), 也可以写成Thread2 runnable = new Thread2() 。第一种写法是为了让代码解耦合,不过度依赖某个类。
- 使用匿名内部类,继承Thread方法,重写run()方法
public class ThreadDemo3{
public static void main(String[] args) {
Thread t = new Thread(new Thread() {
@Override
public void run() {
System.out.println("hhh3");
}
});
t.start();
}
}
匿名类,就是没有名字的类;这种类也没有构造方法,内部类就是在类里面的类;匿名内部类就是没有名字并且位于某个类的内部的类。 匿名内部类可以作为接口的实现类或者作为类的子类,用于扩展接口类或者普通类。
//内部类创建的方法,这既是内部类的创建,也是内部类唯一实例的创建
new Thread() { //Thread 可以替换为,具体情况下要继承的类或者要实现的类
//这里可以写匿名内部类的属性或者方法(普通方法或者需要重写的方法)
}
来一个小例子:
匿名内部类,里面的变量都是形参,需要向匿名内部类外捕获变量,作为实参传值到形参里。这样叫做匿名内部类的“变量捕获”。但是捕获的变量有一个要求就是得是常量。
你可能得纳闷了,啊?你这上面的a不是变量嘛,常量不该是final int a = 0; 吗?其实还有种常量叫事实常量。
虽然是变量的样子,但是值后面有被改变,就可以认作是常量。但是后面这个a值发生了变化,就不是事实常量了,编译器就会报错。其实final int a = 0; 和 int a = 0;写法在这里没什么区别,因为要编译通过,后面肯定是不能修改值的,无论是在匿名函数内还是外,都不能修改a的值。
那么如果你就是想改这个a的值也有办法:
法一:搞个新变量获取a的值,然后匿名内部类里面就用这个变量,这个变量后续的值就不再改变。
法二:将a作为类的成员变量,这样内部类访问a,就是类访问另一个类的成员变量的语法规则,就不受变量捕获的限制了。
- 使用匿名内部类,实现Runnable接口,重写run()方法
public class ThreadDemo4 {
// 实现Runnable,重写run使用匿名内部类
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hhh4");
}
});
t.start();
}
}
- 使用lambda表达式,重写run方法
public static void main(String[] args) {
Thread t = new Thread(() -> {
System.out.println("hhh5");
});
t.start();
}
lambda表达式的基本语法是:
()-> { }
()里面是要重写方法的形参,没有就不写,有参数就写,{}是要重写的方法的方法体。 lambda表达式比创建匿名内部类来实现接口,继承类的代码要更加的优雅简洁。
lambda表达式会根据参数类型,返回值类型,来从被使用方法的参数类型的方法中挨个匹配。然后匹配到一个参数类型返回值类型一样的方法。但是如果没有匹配到,编译器就会报错。
lambda表达式的使用是有条件的,必须是函数式接口的方法才能使用lambda表达式。
什么是函数式接口?函数式接口就是这个接口有且只有一个抽象方法。 函数式接口有一个专门的注解@FunctionalInterface,这个注解只是一个标识的作用,没有这个注解,满足接口有且只有一个抽象方法这个条件的接口,也一样是函数式接口。
3.2 Thread的构造方法
我们创建的线程,默认是按照Thread-0,Thread-1,....这个样子来进行编号,但是这样的名字不便于直接分辨出这个线程是干什么的,对于调试bug就不怎么方便,我们可以用带有String name的构造方法,给线程另起一个名字。(起名字对于线程的执行,不会产生什么影响,可以放心大胆的用。)线程的名字可以重复起,常见于多个线程完成同一个任务的情况下。
可以在自己安装jdk的目录下找jconsole.exe,点击运行就可以看到所有java写的线程的运行情况以及线程的名字了。(ps,只能看java写的线程)
构造方法大都都带有Runnable类型的参数,Runnable 可以理解成是“可执行的”,通过这个接口,就可以抽象出一段可以被其他实体来执行的代码。不仅仅可以搭配线程来执行。
我们传入的是 Thread
的子类对象或者 Runnable
对象,本质上都是 Runnable
接口的实现类对象。由于 Thread
类实现了 Runnable
接口,这种传参方式就是"向上转型",实现类可以根据需要自由地重写方法,体现了 Java 中的多态特性。
ThreadGroup这个线程组是java中的概念,和系统内核中的线程组不是一个东西。这里就不详细介绍了。
3.3 Thread的几个常见属性和获取方法
3.3.1 ID
getID() jvm自动分配的身份标识,会保证线程的唯一性。
3.3.2 名称
getName() 可以获取线程的名字
3.3.3 状态
getState() 得到当前线程的状态。进程有就绪态,阻塞态,执行态。线程也有状态,java中对于线程的状态进行了进一步区分,比系统的原生状态更多。
3.3.4 优先级
getPriority() 得到线程的优先级,在java中,设置优先级的效果并不明显,可以对内核调度器的调度产生一些影响,但是系统的底层是随机调度所以影响不大。
3.3.5 是否是后台线程
isDaemon()是否是守护线程。 守护线程,也称为后台线程,与之相对的还有前台线程,(和手机上的前台app,后台app的概念完全不同)。
isDaemon(true) 设置为true表示是后台线程,反正则是前台线程,创建线程不特意设置,默认就是前台线程。后台默默无闻容易被忘记,所以设置成前台线程,不让进程结束,怕你忘记(bushi)
前台线程运行,会阻止进程结束。后台线程的运行不会阻止进程结束。
相当于前台线程是老板,进程就是正式员工,后台线程是实习生。老板还在干活,员工还能比老板走的早吗,就算手上的活都干完了,也得摸会儿鱼等老板走了才能下班。员工要下班关灯了,但是实习生还在干活,员工急着下班,当实习生的不得快快放下手里的事儿,配合人家下班收关灯。
3.3.6 是否存活
isAlive() 表示内核中的线程(PCB)是否存在,java中定义的线程Thread实例的生命周期和内核中PCB的生命周期是不一样的。
start() 之后,才真正在内核中创建出PCB,当run方法运行结束时,内核中的PCB才释放。
3.3.7是否被中断
isInterrupted() 中断一个线程这个说法其实不怎么准确,中断这个词有多重含义,有可能理解差了。操作系统底层和cpu/各种设备上 都有中断的概念。
在线程中中断一个线程更好的说法,应该是,终止一个线程(就是让run()(线程的入口方法)执行完毕,此时PCB(线程)已经从内核中被释放)。
让run()方法提前执行结束,主要还是得看代码的具体实现方式。
比如,可以引入一个标志位,当标志位被修改的时候,run()方法就执行结束。如下图:
我们预想的是,t线程先执行会儿,然后让标志位进行修改从而终止t线程。但是实际情况确是先修改了标志位,然后t线程才开始执行。这是因为操作系统底层对于线程的调度是随机的。main线程(主线程)和t线程谁先执行是不确定的。
我们可以让main线程休眠(sleep)会儿,来确保t线程执行到run()方法在main执行到flag = false;之前。sleep()方法是Thread类的静态方法。在哪个线程里调用这个方法,哪个线程就sleep,传入的休眠时间的单位是毫秒。
run()之前和run()方法结束后,isInterrupted()是false,run()执行时,isInterrupted()是true。
进程和线程的简单介绍就到这了,如果有发现我写的有错误的地方,欢迎在评论区指正~~