目录
- 多线程
- 线程的引入
- 进程和线程的关系
- 多线程可能存在的问题
- 多线程程序的创建
- Thread创建第一个多线程程序
- 线程的抢占式执行
- 查看java进程中的所有线程
- 用Thread的其他方法创建多线程
- 实现Runnable接口
- 使用匿名内部类,继承Thread
- 使用匿名内部类实现Runnable
- 使用Lambda表达式(最简单,推荐写法)
多线程
线程的引入
我们前一节介绍了进程,引入进程这个概念的主要目的,是为了解决并发编程这样的问题
并发编程:CUP已经进入了多核心时代,CUP再往小了做很快就达到经典物理学就会失效,就会到了量子力学的范围里了,实现起来就很困难。想要进一步提高程序的执行速度,就要充分利用CPU的多核资源
其实,多进程编程已经解决并发编程的问题了。已经可以利用cpu多核资源了。
但是进程太重了(消耗资源多&速度慢)
- 创建一个进程,开销比较大。
- 销毁一个进程,开销也比大
- 调度一个进程,开销还是比较大
说进程重,主要就重在资源分配/回收上
为了解决这些问题,线程就应运而生,线程也叫做轻量级进程解决并发编程问题的前提下,让创建,销毁,调度的速度,更快一些。线程的轻就是把申请/释放资源的操作给省下来了。
举个例子就好比工厂加工的流水线。现在工厂的营收非常乐观,想要扩大生产线,那么有两种解决方案。
- 方案1:再买一块低,建造一个工厂再配备这些流水线。
- 方案2:在原有工厂的基础上再加一些机器和生产线,两套生产线共用一套资源。
上面的例子中方案2明显成本比方案1小很多,资源都可以复用之前的。这就是多线程的方案
进程和线程的关系
进程和对线程的关系,是进程包含线程。
一个进程可以包含一个线程也可以包含多个线程(不能没有)。
只启动第一个线程的时候开销比较大,后续线程就省事了。
同一个进程的多个线程之间,共用了进程的同一份资源(主要是指内存和文件描述表)
内存你线程1new的对象,在线程2,3,4里面都可以直接使用。
线程1打开的文件在线程2,3,4里面可以直接使用。
线程相比于进程来说,更轻量
上节进程的调度,相当于每个进程里面只有一个线程这种情况。如果每个进程里面有多个线程,每个线程都独立在CPU上执行 => 线程是操作系统调度的基本单位 每个线程也有自己的执行逻辑(执行流)
一个线程可以通过一个PCB描述。
一个进程里面可能对应着一个PCB也可能对应着多个。
之前介绍PCB里的状态,上下文,优先级,记账信息,都是各个线程自己的。各自记录各自的。但是同一个进程里的PCB之间,pid是一样的,内存指针和文件描述表也是一样的。
总结:
- 进程包含一个及以上线程,线程是轻量级进程
- 一个进程里 若干线程之间共享着 pid ,内存指针,文件描述表
- 每个线程独立调度执行,享有自己的进程调度相关属性(状态,上下文,优先级,记账信息)
- 进程是操作系统资源分配的基本单位
- 线程是操作系统调度执行的基本单位
多线程可能存在的问题
我们就以工厂流水线为例
- 工厂为了加快生产速度多引入了几条流水线,这样确实可以增加速度,但是如果引入流水线太多,也不是一直能够提高速度的。工厂空间不够(类比CPU核心数量有限),太过拥挤,大家互相推推嚷嚷,会导致正在工作的流水线无法正常工作。
线程太多,核心数目有限,不少开销反而浪费在线程调度上了 - 流水线加工过程中由于各个流水线都在同一个车间中,原料是共享的如果两个流水线同时加工一件商品的时候,加工原料归属会出现问题。
在多进程中,就不会出现这种情况(多进程是把产品分好了,自己加工自己的) - 假如两个流水线的生产的产品有特定编号,每个流水线负责一部分组件相互配合,如果加工错误,可能会导致整个工厂的流水线都不能进行工作了。
如果一个线程抛出异常处理不好的话,很可能把整个进程都带走了,其他线程也挂了。
多线程程序的创建
关于线程的操作,操作系统提供了API。
java是个跨平台语言,很多操作系统提供的功能,都被JVM给封装好了。
在java研发学习过程中,不需要学习系统原生的API(C语言),只需要学习Java提供的API就行了。
Thread创建第一个多线程程序
Java操作多线程,最核心的类Thread
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello world");
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();//线程里的特殊方法,启动一个线程
}
}
输出结果hello world
这个结果和直接输出hello world有什么不同呢?
直接调用t.run和直接调用t.start有什么区别呢?
这个MyThread类继承了Thread类重写了其中的run方法。
如果只打印hello world,这个Java进程主要只有一个线程(调用main方法的线程),主线程。通过t.start()主线程调用t.start,创建出一个新的线程,新的线程调用t.run。
main是一个线程, t.start()又启动了一个线程。开启了多线程程序的运行。
run只是描述了线程要做的工作。start是真正的在操作系统中搞了个线程,并让新线程调用run.
也就是调用操作系统的API,通过操作系统内核创建新线程的PCB,并且要把执行的指令交给这个PCB,当PCB被调度到CPU上执行的时候,也执行到了线程中的run方法中的代码。
线程的抢占式执行
class MyThread extends Thread{
@Override
public void run() {
while (true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
while (true){
System.out.println("hello main");
try {
Thread.sleep(1000);//线程休眠1000ms后面会具体介绍这个方法
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
操作系统调度线程的时候,抢占式执行,具体哪个线程先上,哪个线程后上,不确定,取决于操作系统调度器具体实现策略。虽然有优先级,但是在应用程序层面无法修改。从应用程序(代码)的角度,看到的效果,就好像是线程之间的调度顺序是随机的一样。内核里并非是随机的,但是干预因素太多,并且应用程序这一层也无法感知到细节,只能认为是随机的了。
查看java进程中的所有线程
可以使用jdk自带的工具jconsole 查看当前java进程中的所有线程
-
找到该目录下的jconsole
-
进入自己运行的进程
进入后如果发现出现
如果你是初学,请不必担心,你电脑没有什么重要的数据。 -
找到线程
调用栈,描述了当前方法之间的调用关系
用Thread的其他方法创建多线程
Java实现多线程的方法有很多种上面实现的第一个多线程程序用的就是继承Thread,重写run
实现Runnable接口
//Runnable 作用,是描述一个"要执行的任务", run 方法就是任务的执行细节
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello Thread");
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
//这里只是描述了个任务
Runnable runnable = new MyRunnable();
//把这个任务交给线程来执行
Thread t = new Thread(runnable);
t.start();
}
}
这种方式的好处是解耦合。目的是让线程和线程要干的活之间分离开。
未来如果需要改代码,不用多线程,使用多进程,或者线程池,或者协程池……此时的代码改动比较小
使用匿名内部类,继承Thread
public class ThreadDemo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello world");
}
};
t.start();
}
}
- 创建了一个Thread的子类(子类没有名字)所以才叫做匿名
- 创建了子类的实例,并让t引用指向了该实例.
使用匿名内部类实现Runnable
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello world");
}
});
t.start();
}
}
写法和上面实现Runnable接口本质相同。只不过把实现Runnable的任务交给了匿名内部类语法。此处创建了一个类,实现Runnable,同时创建了类的实例,并且传给Thread的构造方法
使用Lambda表达式(最简单,推荐写法)
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello world");
});
t.start();
}
}
把任务用lambda表达式来描述,直接把lambda传给Thread的构造方法。