文章目录
- 一. 认识线程(Thread)
- 1.1 概念
- 1.1.1 什么是线程
- 1.1.2 线程存在的意义
- 1.1.3 进程和线程之间的区别和联系
- 1.1.4 Java的线程和操作系统的线程
- 1.2 创建线程
- ① 继承Thread类
- ② 实现Runnable 接口
- 对比两种方法
- ③ 变形写法
- ④ 其他写法
- 1.3 查看线程
一. 认识线程(Thread)
1.1 概念
1.1.1 什么是线程
进程进一步细化为线程, 是程序内部的一条执行路径. 一个进程中至少有一个线程. 每个线程之间都可以按照顺讯执行自己的代码. 多个线程之间"同时"执行多份代码.
1.1.2 线程存在的意义
① “并发编程"成为"刚需”
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU 资源.
- 有些人物场景需要"等待IO", 为了让等待IO的时间能去做一些其他的工作, 也需要用到并发编程.
② 虽然多进程也能实现并发编程, 但是线程比进程跟轻量.
- 创建和销毁的速度更快
- 调度的速度更快
③ 线程虽然比进程轻量, 但是人们还不满足, 于是又有了"线程池(ThreadPool)" 和 “协程(Coroutine)”.
1.1.3 进程和线程之间的区别和联系
- 进程包含线程, 都是实现并发编程的方式. 每个进程至少有一个线程, 即主线程.
- 进程和进程之间不共享内存空间,可以保持系统的稳定性; 同一个进程的线程之间共享同一个内存空间(操作系统的线程), 一旦一个线程出现异常, 可能会导致整个进程异常, 容易出现线程安全问题.
- 一个PCB描述一个线程, 多个PCB描述一个进程. 一个进程中的所有PCB内的内存指针和文件描述符表是一样的, 但是上下文, 状态, 优先级等属性是不一样的. 这也说明了, 同一个进程中的线程共用同一份资源(内存 + 硬盘), 但是每个线程独立去CPU上调度.
- 进程是操作系统进行资源分配的基本单位, 而线程是操作系统进行调度执行的基本单位. 创建进程的时候已经分配了资源, 后续创建进程, 直接共用之间的资源即可.
1.1.4 Java的线程和操作系统的线程
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用.
Java中的线程是对系统级别的线程的封装和抽象.
操作系统级别下, 同一个进程的线程之间共享同一个内存空间;
而Java中的线程有私有空间.
1.2 创建线程
① 继承Thread类
-
创建一个线程类, 继承自Thread类
-
重写run方法: 线程入口方法, 描述线程执行的动作.
class MyThread extends Thread { @Override public void run() { System.out.println("这里是线程运行的代码"); } }
-
创建MyThread类的实例
MyThread t = new MyThread();
-
调用 start 方法启动线程
t.start(); // 线程开始运行
- 这个操作就会在底层调用操作系统提供"创建线程"的API, 同时就会在操作系统内核里面创建出对应的pcb结构, 并且加入到对应的链表中.
- 此时, 这个新创建出来的线程就会参与到CPU的调度中, 这个线程接下来要执行的工作, 就是刚刚上面重写的run方法.
代码解析:
class MyThread extends Thread {
@Override
public void run() {
while (true) {
System.out.println("MyThread");
}
}
}
public class Main {
MyThread t = new MyThread();
t.start();
while (true) {
System.out.println("Main");
}
}
第十行
t.start()
创建出了新的线程, 执行run()
中的代码.原来的主线程继续往下执行
主线程和新线程是并发执行的关系, 根据操作系统的调度执行.
注意:
- 如果自己手动调用run()方法, 那么就只是普通方法, 没有启动多线程模式.
- run()方法由JVM调用, 什么时候调用, 执行的时候都由操作系统的CPU调度决定.
- 想要启动多线程, 必须调用start方法
- 一个线程对象只能调用一次start()方法启动, 如果重复调用了, 则将抛出以上的异常"
IllegalThreadStateException
"
② 实现Runnable 接口
-
实现Runnable接口
class MyRunnable implements Runnable { @Override public void run() { System.out.println("这里是线程运行的代码"); } }
-
创建Thread类的实例, 调用Thread的构造方法将Runnable对象作为参数
Thread t = new Thread(new MyRunnable());
-
调用start方法
t.start();
通过实现Runnable接口, 使得该类有了多线程的特征. 所有的分线程要执行的代码都在run方法里面.
在启动多线程的时候, 需要先通过Thread类的构造方法Thread(Runnable target)
构造出对象, 然后调用Thread对象的start方法来运行多线程代码.
实际上, 所有的多线程代码都是通过运行Thread.start()
来运行的, 因此, 不管是继承Thread类还是实现Runnable接口来实现多线程, 最终还是通过Thread的对象的API来控制流程的.
说明: Runnable对象仅仅作为Thread对象的target, Runnable实现类里包含的run()方法仅作为线程执行体. 而实际的线程对象依然是Thread实例, 只是该Thread线程负责执行其target的run()方法.
这一方法, 将线程要执行的任务和线程本身, 进一步解耦合了.
对比两种方法
联系:
Thread类实际上也是实现了Runnable接口的类
public class Thread extends Object implements Runnable
区别:
- 继承Thread: 线程存放Thread子类run方法中.
- 实现Runnable: 现成代码存放在接口的子类的run方法.
实现Runnable接口比继承Thread类所具有的优势
- 避免了单继承的局限性
- 多个线程可以共享同一个接口实现类的对象, 非常适合多个相同线程来处理同一份资源
- 增加程序的健壮性, 实现解耦操作, 代码可以被多个线程共享, 代码和线程独立.
③ 变形写法
-
匿名内部类创建Thread子类的对象.
public class Demo3{ Thread t1 = new Thread() { @Override public void run() { System.out.println("使用匿名类创建 Thread 子类对象"); } }; t1.start(); }
说明:
①
new Thread()
: 创建一个子类, 继承自Thread, 这个子类是匿名的, 并且是在Demo3这个类的内部创建的.② 在子类中重写了run方法
③ 创建了该子类的实例, 并且使用t1这个引用来指向.
-
匿名内部类创建Runnable子类的对象
Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("使用匿名类创建 Runnable 子类对象"); } });
说明:
①
new Runnable()
: 创建一个类, 实现Runnable接口.② 在类中重写了run方法
③ 创建了该类的实例, 并把这个实例作为参数传给了Thread的构造方法.
-
lambda表达式创建Runnable子类对象
Thread t3 = new Thread(() -> System.out.println("使用匿名类创建 Thread 子类对象")); Thread t4 = new Thread(() -> { System.out.println("使用匿名类创建 Thread 子类对象"); });
④ 其他写法
- 基于Callable
- 基于线程池
1.3 查看线程
找到当前项目使用的jdk的位置, 并双击打开
双击bin目录, 找到jconsole.exe
双击jconsole.exe
, 会出现以下窗口
选择本地进程, 并找到当前运行的
线程名称, 再点击连接
若出现以下窗口, 选择不安全连接.
出现以下窗口后, 点击线程
左下角就列出了所有的线程, 不仅有主线程和自己创建的线程, 还有JVM自带的线程.
点击某个线程, 就能得到该线程的具体信息