1.线程(thread)
如果把线程想象成是一个工厂,线程就是若干个流水线:
- 线程其实是包含在进程中的
- 一个进程中可能会有多个线程
- 每个线程都有一段自己要执行的逻辑(指令),每个线程都是一个独立的“执行流”
- 同一个进程中的很多线程之间,是共享了一些资源
所谓的“线程”可以理解成一种轻量级“进程”,也是一种实现并发编程的方式,创建一个线程比创建一个进程成本低,销毁一个线程,比销毁一个进程成本低。
成本低的原因是,新创建一个线程,不需要给这个线程分配很多新的资源,大部分资源都是和原来线程共享的,如果新创建一个进程,就需要给这个进程分配较多的资源
实际进行并打编程的时候美多线程方式要比多进程方式更常见,也效率更高。
同一个进程的多个线程之间共享的资源主要是两方面:
- 内存资源(但是两个不同进程之间的内存不能共享)
- 打开的文件
但是也有一些不是共享的资源:
- 上下文/状态/优先级/记账信息(每个线程要独立参与的 CPU 的调度)
- 内存中有一块特殊的区域——栈空间是每个线程要独立一份
进程是操作系统分配资源的最小单位,线程数操作系统进行执行调度得到最小单位。所谓的操作系统进行进程调度,本质上就是操作系统针对这个进程的若干个线程进行调度。
操作系统是如何管理线程的呢?
本质上和管理进程一样。
- 先描述:用 PCB 描述
- 再组织:用一个双向链表来组织
内核只认 PCB,一个线程和一个 PCB 对应,一个进程可能和多个 PCB 对应。
最开始的计算机是串行工作的,此时没有进程和线程的概念,后来计算机支持多任务并发执行了,在后来经常用多进程实现并发买不太好用,在后来,线程虽然比进程轻量,但是还是不够轻量(存在一个抢占式执行的问题),引入了“协成”的概念(轻量级线程)
一个进程中能搞多少个线程?
- CPU 的个数相关
- 和线程执行的任务类型相关——>1)CPU密集型:程序就一直在执行计算机任务;2)IO 密集型:程序没怎么进行计算,主要是进行输入输出操作
假设这个主机有 8 核 CPU,任务又完全用 CPU 计算,此时线程的数目大概是8个左右,如果任务完全 IO 密集型,理论上线程多少都可以(这是两种极端的情况),显示中的情况是要介于两者之间——>实践中一般需要通过 测试 的方式来找到合适的线程数
对于一个业务比较复杂的服务器来说,上面有几十个线程也是很常见的现象
2.创建线程
时间戳定义:
以 1970年1月1日0时0分0秒为基准时刻,计算当前时刻和基准时刻之间的秒数、毫秒数、微秒数的时间差
创建线程的几种写法:
- 通过显式继承 Thread 类的方式来实现
- 也可以通过匿名内部类的方式来继承 Thread
- 显式创建一个类,实现一个 Runnable 接口,然后把这个 Runnable 的实例关联到 Thread 实例上
- 通过匿名内部类实现 Runnable 接口
- 使用 lambda 表达式来指定线程执行的内容
这几种创建线程的方式,没有本质上的区别,核心都是依靠 Thread 类。只不过指定线程执行的任务的方式有所差异
细节的上的区别:
- 通过 Runnable 、 lambda 的方式来创建线程和继承 Thread 类相比,代码耦合性更小一点,在写 Runnable 或者lambda 的手 run中没有涉及到任何的 Thread 相关内容,这就意味着很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易改成不并发的方式执行
2.1 继承 Thread 方法创建线程
public class Test0804 {
static class Mythread extends Thread {
@Override
public void run() {
System.out.println("Hello ,I am a thread!");
}
}
/**
* 创建线程需要使用 Thread 类,来创建一个 Thread 实例
* 另一方面还需要给这个线程指定要执行哪些指令、代码
* 指定指令的方式有很多种,此处直接继承 Thread 类
* 重写 Thread 类中的 run 方法
* 【注意】放 Thread 对象被创建出来的时候,内核中并没有随之产生一个线程(PCB)
* 需要调用 start 方法
* @param args
*/
public static void main(String[] args) {
Thread t = new Mythread();
//执行这个 start 方法才是真正创建出了一个线程
//此时内核中才随之出现了一个 PCB ,这个 PCB 就会让 CPU 来执行该线程的代码
t.start();
}
}
上面的代码涉及到两个线程——MyThread 创建出来的线程,main 方法对应的主线程
jconsole
为了进一步的观察当前确实是两个线程,可以借助第三方工具来查看该线程的情况,JDK 中内置了一个 jconsole 这样的程序 ,jconsole 在 jdk 目录下的 bin 目录下
具体操作见 如何用 JConsole 查看线程
从折线图可以看到我们现在大概有 13 个线程,在这个折线图线面可以看到具体的线程
这些线程就都是这个 Java 进程中包含的线程
点击这些线程就可以看到具体的信息
图中的 RUNNABLE 相当于就绪状态,堆栈跟踪部分就是线程的调用栈
2.2 匿名内部类方法创建线程
创建了一个没有名字的类,只知道这个类继承自 Thread,“{}” 中是这个类的具体diamante,同时也会 new 出来这个类的实例
演示一下多线程并发执行和单线程得到区别
package com.Test0805;
/**
* Create with IntelliJ IDEA
* Description:串行和并发的执行效果展示
* User:Zyt
* Date:2020-08-05
*/
public class ThreadDemo1 {
private static long count = 100_0000_0000L;
public static void main(String[] args) {
//serial();//串行
//concurrency();//并发
}
private static void concurrency() {
long beg = System.currentTimeMillis();
Thread t1 = new Thread(){
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
}
};
t1.start();
t2.start();
try {
//线程等待,让主线程等待 t1 和 t2 执行结束,然后再继续往下运行
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//t1 t2 和主线程都是并发执行的
//调用了额t1.start()和 t1.start() 之后,两个线程正在计算过程中
//此时主线程仍然会继续执行,下面的 end 就随之被计算了
//正确做法保证 t1 和 t2 都计算完毕再来计算 end 的时间戳
long end = System.currentTimeMillis();//计算结束时间戳
System.out.println("time:" + (end - beg) + "ms");
}
private static void serial() {
long beg = System.currentTimeMillis();//计算开始时间戳
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
int b = 0;
for (long i = 0; i < count ; i++) {
b++;
}
long end = System.currentTimeMillis();//计算结束时间戳
System.out.println("time:" + (end - beg) + "ms");
}
}
时间戳,以 ms 为单位
串行运行效果
并发执行效果
可以看出并发执行时间要比串行时间短
2.3 显式创建一个类,实现 Runnable接口,然后把这个 Runnable 的实例关联到 Thread 上
public class ThreadDemo3 {
/**
* 显式创建一个类,实现 Runnable接口,然后把这个 Runnable 的实例关联到 Thread 上
* Runnable 本质上就是描述了一段要执行的任务代码是什么
* @param
*/
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是一个新线程");
}
}
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
执行效果
2.4 用匿名内部类来实现 Runnable 接口
public class ThreadDemo4 {
/**
*
* @param args
*/
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("我是一个新线程2!");
}
};
Thread t= new Thread(runnable);
t.start();
}
}
执行效果
2.5 使用 lambda 表达式来指定线程执行的内容
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t= new Thread(() -> {
System.out.println("我是一个新线程3");
});
t.start();
}
}
执行效果
Thread 的 run 和 start 之间的区别?
答:
run 只是一个普通的方法调用,没有创建新线程,输出语句是在原线程中执行的,而 start 是要创建一个新线程,由新的线程来执行输出