内存管理:空间上划分
【内核使用的内存】【分配给普通进程使用的内存】【空闲空间】
【进程A】【进程B】 空间划分不保证是连续的
1. Java 应用程序员眼中的内存
JVM 的内存空间分为 栈区、堆区、方法区...(这是属于 Java 应用和 JVM 之间的概念)
JVM 作为 OS(操作系统)眼中的普通进程
2.线性地址(虚拟地址) VS 物理地址
物理地址:真实的内存中的地址
线性地址:物理地址被操作系统进行转换后的一个地址
这个图说明了,我们在线性地址上可能地址是一样的,但是映射到物理内存上,就是不同的。
没有线性地址之前,程序中,同一个程序多次运行,会生成不同的进程,不一定能保证同一个进程,一定被放到内存的同一个位置
所以,引入线性地址后,我们就不需要考虑这些问题了,这个就交给MMU来管理了。
OS分配出来的空间只是线性地址空间,实际的物理内存,可以延迟到,要访问这段内存时再分配。
举个例子: A 找 B 租房子,需要 A 套房子。B 承诺 101 到 200 房间给 A 用(只是分配了线性地址出去)。如果 A 租下房子后没有使用,则 B 并不需要真正给 A 准备 100 套房子。什么时候 A 真正需要了某个房子了, B 再去给 A 找到房子对应的房号即可。
3.进程间通信
理论上进程间是独立的,但实际中,往往是多个进程之间互相配合,来完成复杂的工作。
比如:通过 workbench 进程 和 mysql 服务器进程进行通信,来实现数据的增删查改。
因此就有了进程之间交换数据的必要性了
当下问题:OS 进行资源分配是以进程为基本单位进行分配的,包括内存。分配给 A 进程的内存不会分配给 B 进程。所以,进程A、B 之间直接通过内存来进行数据交换的可能性完全不存在了。
所以 OS 需要提供一套机制,用于让 A、B 进程之间进行必要的数据交换——进程间通信。
进程间通信的常见方式:
- 管道(pipe)
- 消息队列(message queue)
- 信号量(semaphore)
- 信号(signal)
- 共享内存(shared memory)
- 网络(network)—— workbench 和 mysqld 通信的方式
4.进程(process)和线程(thread)的关系
进程-线程 :1 :m 的关系
一个线程一定属于一个进程;一个进程下可以允许有多个线程
一个进程内至少有一个线程,通常被这个一开始就存在的线程,称为主线程(main thread)
主线程和其他线程之间低位是完全相等的,没有任何特殊性。
为什么 OS(操作系统)要引入 线程 这一概念?
进程这一概念天生就是资源隔离的,所以进程之间进行数据通信注定是一个高成本的工作。
现实中,一个任务需要多个执行流一起配合完成,是非常常见的
所以,需要一种方便数据通信的执行流概念出来,线程就承担了这一职责。
5.什么是线程(OS 系统层面上的线程)
线程是 OS 进行 调度(分配 CPU) 的基本单位
- 进程:OS 进行资源分配的基本单位
- 线程:OS 进行调度的单位
线程变成了独立执行流的承载概念,进程退化成只是资源(不含 CPU )的承载概念
比如:运行一个程序,没有线程之前,OS 创建进程,分配资源,给定一个唯一的 PC,进行运行。有了线程之后,OS 创建进程,分配资源。创建线程(主线程),给定一个唯一的 PC,进行运行。
程序的一次执行过程表现为一个进程,main 所在的线程就是主线程。主线程中可以运行对应的操作来创建运行其他线程。
只针对 OS 级别的线程:OS 针对同一个进程下的线程实现“连坐”机制:一旦一个线程异常退出,OS 会关闭该线程所在的整个进程。
线程 VS 进程
- 概念区别
- 由于进程把调度单位这一职责让渡给了线程,所以,使得单纯进程的创建销毁适当简单
- 由于线程的创建销毁不涉及资源分配、回收的问题,所以,通常理解,线程的创建/销毁成本要低于进程的成本
6.JVM 中规定的线程
“Java线程” VS “OS 线程(原生线程)”
不同的 JVM 有不同的实现,它们外在表现基本一致,除了极个别的几个现象。Java线程,一个线程异常关闭,不会连坐。我们使用的HotSpot实现(JVM)采用,使用一个 OS 线程来实现一个 Java 线程。
Java中由于有 JVM 的存在,所以使得 Java 中做多进程级别的开发基本很少。Java 中的线程还克服了很多 OS 线程的缺点。所以,在 Java开发中,我们使用多线程模型来进行开发,很少使用多进程模型。
1.Java 线程在 代码中是如何体现的
java.lang.Thread 类(包括子类)的一个对象 Thread——线程
2.如何让在代码中创建线程(最基本)
- 通过继承 Thread 类,并重写 run 方法。实例化该类的对象 -> Thread 对象
/*
1.继承 java.long.Thread 类
2.光继承的情况,这个线程中一条指令都没有
所以我们需要给线程书写让它执行的指令(以语句的形式)
*/
public class MyFirstThreadClass extends Thread{
@Override
public void run() {
// 这个方法下写的所有代码,如果正确创建线程的话,都会运行在新的线程执行流中
System.out.println("这是我的第一个线程");
}
}
public class Main {
public static void main(String[] args) {
MyFirstThreadClass t = new MyFirstThreadClass();
// t 指向了一个创建出来的线程对象
// 线程创建好了,但并没有运行起来
}
}
只是创建线程,并没有启动线程,运行的结果并不会打印什么
2.通过实现 Runnable 接口,并重写 run 方法。实例化 Runnable 对象。利用Runnable 对象去构建一个Thread 对象。
/*
1.实现 java.long.Runnable 接口
2.通过重写 run方法,来指定任务要做的工作
这个线程要不要交给一个新的线程去执行,这里是不知道的
*/
public class MyFirstTask implements Runnable{
@Override
public void run() {
System.out.println("这是我的第一个任务的第一句话");
}
}
public class Main {
MyFirstTask task = new MyFirstTask(); // 创建了一个任务对象
Thread t = new Thread(task); // 把 task 作为 Thread 的构造方法传入
// 让这个新创建的 Thread 去执行 task 任务
// 语句就运行在新的线程中
// 这里只是创建线程,暂时还没有运行
}
3.启动线程
当手中有一个 Thread 对象时调用其 start()方法
注意:1.一个已经调用过 start()不能再调用start()了,再调用就会有异常发生
2.不要调用成 run()
对于 Java 应用程序,我们需要通过 Thread 对象来控制线程的一切。
启动线程:
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("正在执行");
}
}
public class Main {
public static void main(String[] args) {
MyThread t = new MyThread();
// 通过调用 Thread 对象的 start 方法,来开始线程的运行
t.start();
}
}
4.start()做了什么?
t.start() 只做了一件事:把线程的状态从新建变成了就绪。不负责分配CPU
线程把加入到 线程调度器(不区分是 OS 还是 JVM 实现的)的就绪队列中,等待被调度器选中分配 CPU。
从子线程进入到就绪队列这一刻起,子线程和主线程在低位上就完全平等了
所以,哪个线程被选中去分配 CPU 就听天由命了。
先执行子线程中的语句还是主线程中的语句理论上是都有可能的
但大概率是主线程中的打印先执行? 为什么?
t.start() 是主线程的语句。因此,这条语句被执行了,说明主线程现在正在 CPU 上。(主线程是运行状态),所以,主线程刚刚执行完 t.start() 就马上发生线程调度的概率不大。
所以,大概率还是 t.start() 的下一条语句就先执行了。
public class MyThread extends Thread{ @Override public void run() { System.out.println("我是 MyThread 类下的 run 方法中的语句,会运行在子线程中"); } }
public class Main { public static void main(String[] args) { MyThread t = new MyThread(); t.start(); System.out.println("我是 main 类下的 main 静态方法中的语句,会运行在线程主线程中"); } }
什么时候子线程中的语句会先执行?
- 刚刚执行完 t.start() 之后,输出(“sout”)之前,发生了依次线程调度
- 主线程状态:运行 => 就绪。主线程不再持有 CPU。意味着主线程的下一条语句不再执行
- 调取的时候,选中子线程调度 子线程状态:就绪 => 运行。子线程持有了 CPU ,所以,执行到子线程语句
注意:
- 一个已经调用过 start()不能再调用start()了,再调用就会有异常发生
- 不要调用成 run()
t.start() 只允许工作在“新建”状态下,调用两次 start()会出选非法的线程状态异常
调用 run 方法,就和线程没关系了。相当于是完全在主线程下运行代码。
5.什么情况下,会出现线程调度(开始选择一个新的线程分配 CPU)
1.CPU 空闲
当前运行着的 CPU 执行结束了 运行 => 结束
等待外部条件 运行 => 阻塞
主动放弃 运行 => 就绪
2.被调度器主动调度
1.高优先级线程抢占
2.时间片耗尽(常见)
在多线程中,明明代码是固定的,但会出现现象是随机的可能性,主要原因就是调度的随机性体现在线程的运行过程中。
同一个程序启动多个线程:
public class Main { // 同一个程序启动多个线程 public static void main(String[] args) { MyThread t1 = new MyThread(); t1.start(); MyThread t2 = new MyThread(); t2.start(); MyThread t3 = new MyThread(); t3.start(); MyThread t4 = new MyThread(); t4.start(); } }
7.线程和方法调用栈的关系
每个线程都有自己独立的调用栈
由于每个线程都是独立的执行流,A 线程调用过哪些方法,和 B 线程根本就没有关系。表现为每个线程都有自己的独立的栈。
调用同一个方法:说明执行的是同一批指令
栈不同(帧不同):说明执行指令时,要处理的数据是不同
8.线程中最常见的属性:
1.id 本进程(JVM进程)内部分配的唯一的 id 只能 get 不能 set
2. name(名字) 为了给开发者方便看的,JVM 并不需要这个属性
默认情况下,如果没有给过名字,线程名字遵守 Thread-... 。第一个是 Thread-0、Thread-1、Thread-2。
public class Main1 {
static class MyThread extends Thread{
@Override
public void run() {
System.out.println(this.getName());
}
}
public static void main(String[] args) {
Thread thread = Thread.currentThread(); // 当前线程
System.out.println(thread.getName()); // 主线程名称
// 子线程
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
MyThread t3 = new MyThread();
t3.start();
MyThread t4 = new MyThread();
t4.start();
}
}
可以get 也可以set,可以通过 setName(...)设置,也可以通过 Thread(...)构造方法设置。
public class Main2 {
static class MyThread extends Thread {
// // 方法一:通过 setName(...)设置
// public MyThread(){
// setName("我是方法一");
// }
// 方法二:通过 Thread(...)构造方法设置
public MyThread(){
super("我是方法二"); // 调用父类(Thread)的构造方法
}
@Override
public void run() {
System.out.println(this.getName());
}
}
public static void main(String[] args) {
Thread thread = Thread.currentThread(); // 当前线程
System.out.println(thread.getName());
MyThread t1 = new MyThread();
t1.start();
MyThread t2 = new MyThread();
t2.start();
MyThread t3 = new MyThread();
t3.start();
}
}
通过传入参数的方法可以分别改名:
public class Main3 {
static class MyThread extends Thread {
public MyThread(String name){
super(name); // 调用父类(Thread)的构造方法
}
@Override
public void run() {
System.out.println(this.getName());
}
}
public static void main(String[] args) {
Thread thread = Thread.currentThread(); // 当前线程
System.out.println(thread.getName());
MyThread t1 = new MyThread("我是t1");
t1.start();
MyThread t2 = new MyThread("我是t2");
t2.start();
MyThread t3 = new MyThread("我是t3");
t3.start();
}
}
id 就相当于一个线程的身份证号(出生被分配,无法被修改,不能重复);
name 就相当于一个线程的名称(可以重复、可以被修改)。
t1、t2、t3这三个线程是由 主线程 执行创造出来的,所以取名为“子线程”。创造上有父子关系,但调度时低位平等。t1、t2、t3 线程是一个动态的过程,MyThread 类只是给 线程 去执行的程序,是一个静态内容。
3.在 Java 代码中看到的线程状态
1)理论中的状态
2)Java 代码中实际看到的状态
线程可以 get/set 自己的优先级。这个优先级的设置,只是给JVM 一些建议,不能强制让哪个线程先被调度。
9.前台线程 VS 后台线程(精灵线程、守护线程)
前台线程一般是做一些有交互工作的
后台线程一般是做一些支持工作的线程
比如:写了一个音乐播放器
1.线程响应用户点击动作(前台)
2.线程去网络上下载歌曲(后台)
我们创造出来的线程默认都是前台线程,除非修改。
JVM 进程什么时候才退出:所有的前台线程都退出了,JVM 进程就退出了
- 必须要求所有前台都退出,和主线程没关系
- 和后台线程没关系,即使后台线程还在工作,也正常退出
10.总结 线程的 场景属性
我们都是通过 Thread 对象进行操作
id(get)、name(get/set)、状态(get)、优先级(get/set)、前后台线程(get/set)
11.JDK 中自带的观察线程的工具
JVM 运行中的一些相关情况,比如内存、类加载情况、线程情况
TimeUnit.SECONDS.sleep(1); 休眠 1s 后再继续运行
12. Thread.join() 方法
- b = new B(); b.start();
- 吃饭
- b.join(); // 这个方法会阻塞,直到 B 运行结束才返回
- 这个时候 B 一定已经结束了
- 打印 b 结束了
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
public class Main {
private static class B extends Thread {
@Override
public void run() {
super.run();
// 模拟 B 要做很久的工作
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
println("B:我的任务已经完成");
}
}
private static void println(String msg){
Date date = new Date();
DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(format.format(date) + ": " + msg);
}
public static void main(String[] args) throws InterruptedException {
B b = new B();
b.start();
println("A 自己先去吃饭");
// 有 join 和 没有join 的区别
b.join();
println("B 送来了钱,结账走人");
}
}
如果没有 b.join() :B任务还没完,没给A 送钱 ,A就走了,这是不对的
join(); 等待线程死去
join(long millis);等待线程死去,但最多等 ? 毫秒
join(long millis,int nanos);有毫秒有纳秒,时间更精确
对比以下两种方法:
1.多核环境下,并发排序的耗时 < 串行排序的耗时
单线程一定跑在一个 CPU (核)上,多线程意味着可能工作在多个核上(核亲和性)
2.单核环境下,并发排序的耗时也能小于吗?
即使在单核环境下,并发的耗时也可能较少。
本身,计算机就有很多线程在等待分配 CPU ,比如,现在有100个线程。意味公平的情况下,我们的排序主线程,只会被分配1/100 的时间。
当并发时,我们使用 4 个线程分别排序,除其他的 99 个之外,计算机中共有 99 + 4 = 103 个线程。
我们 4 个线程同属于一个进程,分给我们进程的时间占比 4/103 > 1/100.
所以,即使单核情况下,我们一个进程中的线程越多,被分到的时间片是越多的。
线程越多越好吗?
no。1.创建线程本身也不是白嫖的。
2.即使理想情况下,不考虑其他耗时,极限也就是 100%。线程调度也需要耗时(OS 从 99 个线程中挑一个的耗时 和 从 9999 个线程中挑一个的耗时不同)
CPU 是公共资源,写程序的时候也是要考虑公德心的。如果是好的 OS 系统,可能也会避免这个问题。
3.并发排序的耗时就一定小于串行吗?
不一定。
串行的排序: t = t(排区间1) + t(排区间2) + t(排区间3)+ t(排区间4)
并发的排序:t = 4 * t(创建线程)+ t(排区间1) + t(排区间2) + t(排区间3)+ t(排区间4)+ 4 * t(销毁)
我们要写多线程的代码原因:1.提升整个进程的执行速度(尤其计算密集性的程序)
2.当一个执行流阻塞时,为了还能处理其他任务,可以引入多线程
13. Thread 下的几个常见静态方法
1.Thread.sleep(...)
让线程休眠多少毫秒
TimeUnit.SECONDS.sleep(1) == Thread.sleep(1000)
从线程的状态的角度,调用sleep(?),就是让当前线程 从 "运行 "=> "阻塞"。
阻塞状态实际上就是等待某个条件:要求时间过去 ... 之后,当条件满足时(时间真的过去了 ... ),线程从 阻塞 => 就绪 状态,这个间隔很短,基本对人类无感,当线程被 调度器选中时,开始接着之前的指令执行,表现为sleep 之后的语句执行。从外部表现来讲,就是让线程休眠了一段时间。
2.Thread.currentThread();
Thread 引用,指向一个线程对象,执行的就是 在哪个线程中调用的该方法,就返回哪个对象。
public class Main {
static class MyThread extends Thread{
@Override
public void run() {
printCurrentThreadAttributes();
}
}
private static void printCurrentThreadAttributes(){
Thread t = Thread.currentThread();
System.out.println(t.getId());
System.out.println(t.getName());
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
t1.setName("t1");
MyThread t2 = new MyThread();
t2.start();
t2.setName("t2");
MyThread t3 = new MyThread();
t3.start();
t3.setName("t3");
printCurrentThreadAttributes();
}
}
3.Thread.yield()
让线程让出 CPU 。线程从 运行 => 就绪状态。随时可以继续被调度回 CPU
yield 主要用于执行一些耗时较久的计算任务时,为了防止计算机处于“卡顿”现象,时不时的让出一些 CPU 资源,给 OS 内的其他进程。
public class Main {
static class PrintWhoAnI extends Thread{
private final String who;
PrintWhoAnI(String who) {
this.who = who;
}
@Override
public void run() {
while (true){
System.out.println("我是 " + who);
if(who.equals("张三")){
Thread.yield();
}
}
}
}
public static void main(String[] args) {
PrintWhoAnI 张三 = new PrintWhoAnI("张三");
PrintWhoAnI 李四 = new PrintWhoAnI("李四");
张三.start();
李四.start();
}
}
可以发现张三出现的频率要低于李四,因为张三一直在让李四
14.线程的控制:通知线程停止
A 叫 B 来干活。突发了一些情况,需要让 B 停止工作(即使分配给它的任务还没有完成)。所以A 需要让 B 停止。
1.暴力停止stop(),直接kill掉 B。
目前基本上已经不采用了,因为把 B 直接杀掉,不知道 B 把工作进行的如何了(不可控)
2.和 B 进行协商 interrupt()
A 主动给 B 发一个信号,代表 B 已经停止了(发消息)
B 在一段时间内,看到了停止信号后,就可以主动把手头工作做到一个阶段完成,主动退出。(需要我们写代码完成)
A 主动让 B 停止 b.interrupt();
只是发了一个消息,实际上并不会影响B 的运行
B 如何感知有人让他停止:
情况1:B 正在正常执行代码,可以通过一个方法来判定 static boolean interrupted()
静态方法: Thread.interrupted()——检测当前线程是否被终止
返回 true:有人让我们停止;返回 false:没人让我们停止
情况2: B 可能正处于休眠状态(比如sleep、join),意味着 B 无法立即执行 Thread.interrupted()。此刻,JVM 的处理方式是,以异常形式通知 B :InterruptedException
当 B 处于休眠状态是,捕获了 InterruptedException,代表有人让我们停止,具体要不要停,什么时候停,怎么停,完全自己做主。
15.总结
- 如何在代码中创建、启动线程
- 线程在底层的原理(OS 中的线程 + 执行流)
- 线程结果的随机性
- 线程的常见属性(id,名字,状态,优先级,前后台线程)
- 相关工具:调试工具、jconsole
- 线程之间的协调工作:t.join() 等待 t 结束
- Thread 下的常见方法:sleep(...)、currenntThread(...)
- 休眠、让出 CPU