目录
一、什么是进程/任务(Process/Task)
二、什么是线程(Thread)
三、进程和线程的区别
四、创建线程的方法:
方法1 继承 Thread ,重写run
方法2 实现 Runnable 接口
方法3 匿名内部类创建 Thread 子类对象
方法4 匿名内部类创建 Runnable 子类对象
方法5 lambda 表达式创建 Runnable 子类对象
五、如何查看进程中的线程
六、Thread类中的常见构造方法
七、Thread的常见属性编辑
八、中断线程
方法一:直接自己定义 标志位
方法二:使用 Thread类 中自带的标志位~
九、等待线程 - join()
一、什么是进程/任务(Process/Task)
进程是操作系统对一个正在运行的程序的一种抽象,换言之,可以把进程看做程序的一次运行过程。通俗来说,一个跑起来的应用程序就被成为进程。同时,在操作系统内部,进程又是操作系统进行资源分配的基本单位。
为啥要有进程:进程是为了解决并发编程这个问题,现在的CPU都是多核CPU,利用代码要把CPU的多核心充分利用上。
二、什么是线程(Thread)
一个线程就是一个 "执行流". 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间 "同时" 执行着多份代码.
为啥要有线程:多进程编程已经可以解决很多并发编程的问题了,CPU的很多资源也可以调度起来了。但是,在资源分配和回收上,多进程还是有很多短板。主要有以下三点:
1.创建一个进程,开销比较大。
2.销毁一个进程,开销比较大。
3.调度一个进程,开销也比较大。
进程消耗资源多,速度慢(创建,销毁,调度),资源分配回收效率低,于是就出现了线程(轻量级进程)解决并发编程的前提下,让创建,销毁,调度的速度快一点,主要是把申请资源/释放资源的操作省下来。
三、进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
-
线程模型,天然就是资源共享的,多个线程抢同一个资源,非常容易触发线程安全问题;进程模型,天然就是资源隔离的,不容易触发。进行进程间通信的时候,多个进程访问同一个资源,就可能出现问题
-
进程的上下文切换速度比较慢,而线程的上下文切换速度比较快。
-
进程都拥有自己独立的虚拟地址空间,有多个进程时,其中一个进程崩溃了并不会影响其他进程。但是线程是多个线程共用一个内存空间,当一个线程抛异常,如果处理不好很可能会把整个进程都给带走了,其他线程也就挂了
注意:
- 增加线程数量也不是一直可以提高速度的,CPU核心数量是有限的,线程数量太多开销反而浪费在线程调度上
- 系统创建线程也是要消耗资源的,虽然比进程轻量但也不是0,创建太多线程会导致资源耗尽,导致别的进程用不了
- 同一个进程里的多个线程之间共用了进程的同一份资源(内存和文件描述符表),只有第一个线程启动时开销是比较大的,后续线程就省事了
Java操作多线程最核心的Thread类,不需要导其他包。java.lang包下最基本的类。
四、创建线程的方法:
方法1 继承 Thread ,重写run
1) 继承 Thread 来创建一个线程类
class MyThread extends Thread{
@Override
public void run(){
System.out.println("hello thread");
}
}
2) 创建 MyThread 类的实例
MyThread t = new MyThread();
3) 调用 start 方法创建启动线程,新的线程负责执行t.run();
t.start(); // 线程开始运行
t.run();方法重写//虽然t是父类的引用,此处调用的run仍然是子类的方法(t本质上还是指向了子类的对象)
start 和 run 之间的区别
start是真正创建了一个线程,线程是独立的执行流
run只是描述了这个线程要干的活是啥,如果直接在main中调用run,此时没有创建新线程全是main线程一个在工作
方法2 实现 Runnable 接口
1) 实现 Runnable 接口
class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("hello thread");
}
}
2) 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
3) 调用 start 方法创建启动线程,新的线程负责执行t.run();
t.start(); // 线程开始运行
方法3 匿名内部类创建 Thread 子类对象
public class Main {
public static void main(String[] args) {
Thread t = new Thread (){
@Override
public void run(){
System.out.println("hello thread");
}
};
t.start();
}
}
- 创建一个Thread的子类(子类没有名字,所以才叫匿名)
- 创建了子类的实例,并且让t引用指向该实例
方法4 匿名内部类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
和方法二本质相同,只不过是把实现Runnable任务交给匿名内部类的语法
此处创建了一个类,实现Runnable,同时创建了类的实例并且传给Thread的构造方法
方法5 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
五、如何查看进程中的线程
可以用jdk自带的工具jconsole查看当前的java进程中的所有线程
六、Thread类中的常见构造方法
Thread(String name):
给线程起一个名字,这个名字对于程序的执行没影响,但是对于调试来说还是挺有用的~
如果不手动设置,也会有默认的名字,形如 Thread-0、Thread-1、......
七、Thread的常见属性
这里的后台线程也叫守护线程
- 前台线程会阻止进程的结束,前台线程的工作没做完,进程是完不了的,
- 后台线程不会阻止进程的结束,后台线程的工作没做完,进程照样可以结束,
- 换句话说,进程会保证所有的前台线程都执行完了 才会退出~
我们手写的线程默认都是前台线程,包括main也是默认前台,但是我们可以通过setDeamon()设置成后台线程
public class Demo7 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true) { System.out.println("hello thread"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } },("我的线程")); t.start(); } }
分析:
因为该线程是一个前台线程,需要等待其运行结束,进程才会结束,所以会一直执行下去~
再如:我们可以把他手动设置成后台线程:
isAlive判断当前线程是否还存活,调用start前isAlive是false,调用后isAlive是true
虽然线程把run里要干的事干完后就销毁了,pcb也随之释放了,但是Thread t这个对象还不一定被释放,此时isAlive也是false,t的销毁要等GC回收
总结:
八、中断线程
所谓的 "中断线程",就是让线程尽快把入口方法执行结束~
方法一:直接自己定义 标志位
package J_1125;
public class Main {
//用一个布尔变量来表示 线程是否要结束
//这个变量是一个成员变量,而不是局部变量
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while (!isQuit) {
System.out.println("线程运行中......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新线程执行结束!");
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出!");
isQuit = true;
}
}
运行结果:
这个代码中,控制线程结束,主要是这个线程里有一个循环,这个循环执行完,就算结束了~
方法二:使用 Thread类 中自带的标志位~
Thread 其实内置了一个标志位,不需要咱们去手动创建标志位
Thread.currentThread().isInterrupted()
--currentThread() 是 Thread类的静态方法,获取到当前线程的实例,这个方法中有一个线程会调用它
--线程1 调用这个方法,就能返回线程1 的 Thread对象;
--线程2 调用这个方法,就能返回线程2 的 Thread对象~
--类似于 JavaSE 里面的 this~
--isInterrupted() 为判定内置的标志,可以获取到标志位的值,为 true 表示线程要被中断~
package thread;
public class Demo10 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() ->{
while (!Thread.currentThread().isInterrupted()) {
System.out.println("线程运行中......");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
Thread.sleep(5000);
System.out.println("控制新线程退出!");
t.interrupt();
}
}
说明:
调用了 interrupt,产生了一个异常,异常虽然出现了,但是线程仍然在继续运行
注意理解 interrupt方法的行为:
如果 t 线程没有处在阻塞状态,此时 interrupt 就会修改内置的标志位
如果 t 线程正在处于阻塞状态,此时 interrupt 就让线程内部产生阻塞的方法例如:
- 线程在sleep中休眠,此时调用interrupt会把t线程唤醒,从sleep中提前返回。
- interrupt会触发sleep内部的异常,导致sleep提前返回
上述循环代码中,正好异常被捕获了,而捕获之后,啥也没有干只是打印了一个调用栈interrupt会做两件事:
- 把线程内部的标志位(boolean)设置成true,表示终止
- 如果线程在进行sleep,就会触发异常,把sleep唤醒,sleep又把刚才设置的标志位设置回false(继续),这就表示sleep异常被catch完了之后循环还要继续执行
注意:调用interrupt只是通知终止,线程会不会终止,还得看代码怎么写的
九、等待线程 - join()
线程是一个随机调度的过程
等待线程就是一种确定线程执行顺序的辅助手段,来控制两个线程的结束顺序。
join() 的行为:
- 如果被等待的线程 还没执行完,就阻塞等待~
- 如果被等待的线程 已经执行完了,就直接返回~
main 有点特殊,不太方便 join()~
一般情况下,想让谁阻塞,谁就调用 join() 即可 ~
如:要实现让 t2 等待 t1,main 等待 t2,就可以让main去调用 t2.join(),t2 再调用 t1.join() 图像 小部件