目录
一、认识线程
1.1 线程的概念:
1.2 为什么需要线程:
1.3 面试题.谈谈进程和线程的区别:
1.4 Java的线程和操作系统线程的关系:
二、创建线程
2.1 创建线程的5种写法:
2.1.1 写法1.继承 Thread 类:
2.1.2 写法2.实现 Runnable 接口:
2.1.3 写法3.继承 Thread 使用匿名内部类:
2.1.4 写法4.实现 Runnable 使用匿名内部类:
2.1.5 写法5.使用lambda(推荐写法):
2.2 run方法和start方法的区别:
2.3 多线程的优势:
三、Thread类及常见方法
3.1 Thread 的常见构造方法:
3.2 jconsole使用过程:
3.3 Thread的常见属性:
3.3.1 属性列表:
3.3.2 前后台线程的关系:
一、认识线程
1.1 线程的概念:
一个线程就是一个 "执行流" 。每个线程之间都可以按照顺序执行自己的代码。多个线程之间 "同时" 执行着多份代码。
1.2 为什么需要线程:
(1)首先经过多年的发展,“并发编程” 已成成为 “刚需”。
• 单核CPU的发展遇到了瓶颈.要想提高算力,就需要多核CPU.而并发编程能更充分利用多核CPU资源。
• 有些任务场景需要 "等待IO",为了让等待IO的时间能够去做一些其他的工作,也需要用到并发编程。
(2)其次,虽然多进程也能实现并发编程,但是线程比进程更轻量。
• 创建线程比创建进程更快。
• 销毁线程比销毁进程更快。
• 调度线程比调度进程更快。
(3)最后线程虽然比进程轻量,但是人们还不满足,于是又有了 "线程池" (ThreadPool)和 "协程" (Coroutine)。
本文章主要介绍多线程,有关线程池和协程的概念后续会单独再写文章解释。
1.3 面试题.谈谈进程和线程的区别:
主要有如下四点:
• 进程是包含线程的。每个进程至少有⼀个线程存在,即主线程。
• 进程和进程之间不共享内存空间。同⼀个进程的线程之间共享同⼀个内存空间。
• 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
• 一个进程挂了一般不会影响到其他进程。但是一个线程挂了,可能把同进程内的其他线程一起带走(整个进程崩溃)。
1.4 Java的线程和操作系统线程的关系:
线程是操作系统中的概念。操作系统内核实现了线程这样的机制,并且对用户层提供了一些API供用户使用(例如Linux的pthread库)。
Java标准库中Thread类可以视为是对操作系统提供的API进行了进一步的抽象和封装。
二、创建线程
下面我会提供创建线程的常见的5中写法,希望友友们都要掌握。下面经常会用到run方法和start方法,关于它们的区别,大家可以先把这5中写法看完后,我在后面有写区别🤩🤩🤩。
2.1 创建线程的5种写法:
2.1.1 写法1.继承 Thread 类:
继承 Thread 来创建⼀个线程类。写法如下:
class MyThread extends Thread{
@Override
public void run() {
System.out.println("hello Thread");
System.out.println("Thread end");
}
}
public class demo1 {
public static void main(String[] args) {
Thread t = new MyThread();//向上转型
t.start();//启动线程
}
}
这里解释一下为什么不能直接直接创建一个Thread对象,而是要再写一个Thread的子类:这是因为我们要重写 run 方法,如果不重写,直接调用原生的,会达不到我们的预期,这显然不是我们想看到的。
运行结果:
2.1.2 写法2.实现 Runnable 接口:
• Runnable接口源码:
通过观察其源码我们不难发现这是一个 “函数式接口” ,所以我们后面有一种写法就会利用到lambda表达式,里面涉及到的一些 “变量捕获” 的知识如果友友忘了的话要记得复习呀。
这个相比于第一个写法的好处是:能够起到解耦合的作用,例如当前是通过多线程的方式执行的,未来也可以很方便改成基于线程池的方式执行,也可以改成基于虚拟线程的方式执行(改动成本比较小),而继承Thread的写法基本就只适用于多线程。具体写法如下:
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello Thread");
System.out.println("Thread end");
}
}
public class demo2 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start();
}
}
运行结果:
2.1.3 写法3.继承 Thread 使用匿名内部类:
这个写法的效果和 2.1 的写法效果没有任何区别,因为使用匿名内部类本来就是为了方便。具体写法如下:
public class demo3 {
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("hello Thread");
System.out.println("Thread end");
}
};
t.start();
}
}
运行结果和前面一样就不贴了。
2.1.4 写法4.实现 Runnable 使用匿名内部类:
和写法2效果一样:
public class demo4 {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable(){
@Override
public void run() {
System.out.println("hello Thread");
System.out.println("Thread end");
}
});
t.start();
}
}
2.1.5 写法5.使用lambda(推荐写法):
这个是比较推荐的写法,因为是 “函数式接口” 我们就可以使用lambda表达式来简化写法。具体写法如下:
public class demo5 {
public static void main(String[] args) {
Thread t = new Thread(()->{
System.out.println("hello Thread");
System.out.println("Thread end");
});
t.start();
}
}
上述 5 种写法本质都是要把线程执行的任务内容表示出来,通过 Thread 的 start 来创建 / 启动系统中的线程。Thread 对象和操作系统内核中的线程是一一对应的关系。
2.2 run方法和start方法的区别:
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
• 覆写 run 方法是提供给线程要做的事情的指令清单。
• 线程对象可以认为是把 李四、王五叫过来了。
• 而调用 start() 方法,就是喊⼀声:”行动起来!“,线程才真正独立去执行了。
总而言之:调用 start 方法,才真的在操作系统的底层创建出一个线程。
2.3 多线程的优势:
利用多线程在一些场合可以提高程序的运行速度。具体案例如下:
前置知识:
• 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳。
• serial 串行的完成一系列运算。concurrency 使用两个线程并行的完成同样的运算。
如果对串行和并行不了解的话可以前往:JavaEE前置知识 中查看并行与并发的区别。
public class ThreadAdvantage {
// 多线程并不⼀定就能提⾼速度,可以观察,count 不同,实际的运⾏效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使⽤并发⽅式
concurrency();
// 使⽤串⾏⽅式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利⽤⼀个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运⾏结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
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.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串⾏: %f 毫秒%n", ms);
}
}
案例结果如下:
可以看到速度快了两倍多。不使用多线程的并发就会出现 “一核有难,多核围观” 的现象。
三、Thread类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 Thread 对象组织起来,用于线程调度,线程管理。
3.1 Thread 的常见构造方法:
我们最常使用的是第三个,至于第五个目前在实际开发中更多的是被线程池取代了,这里只演示第三个。演示如下:
public class demo6 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"我的Thread");
t.start();
while(true){
System.out.println("hello Main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
我们可以利用使用 jconsole 命令观察线程。
3.2 jconsole使用过程:
我们利用上面的程序来进行连接,连接的时候要保证程序在运行中。
• 打开 c 盘并进入 Program Files :
• 进入 Java 中的 jdk :
• 进入 bin 后找到 jconsole 后以管理员的身份运行它:
• 看到这个就成功找到 jconsole 了:
记得连接我们运行的java程序。
注意:程序一定要保证在运行状态,比如我们写一个while(true)循环来保证我们连接的时候程序在跑,不然我们是连接不到的。
• 查看结果:
点击线程,在下面我们能看到main和我的Thread(修改命名)。
完成上面步骤我们已经成功利用 jconsole 查看运行的 java 线程。
3.3 Thread的常见属性:
3.3.1 属性列表:
属性解释:
• ID 是线程的唯一标识,不同线程不会重复。
• 名称是各种调试工具用到。
• 状态表示线程当前所处的一个情况。
• 优先级高的线程理论上来说更容易被调度到。
• 关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
• 是否存活,即简单的理解,为 run 方法是否运行结束了。
• 线程的中断问题,下面会单独讲。
3.3.2 前后台线程的关系:
• 前台线程:前台线程如果不运行结束的话,此时 Java 进程是一定不会结束的。
• 后台线程:后台线程即使继续在执行,也不能阻止 Java 进程结束。
我们默认创建的线程都是前台线程。我们可以利用 setDaemon 方法来把线程设置为后台线程。
注意:关于线程的各种属性的设置,都要放在 start 之前,一旦线程已经启动了,那么开弓就没有回头箭,这个时候再设置就来不及了,还会返回一个异常。
测试案例:
public class demo7 {
public static void main(String[] args) {
Thread t = new Thread(()->{
while(true){
System.out.println("hello Thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.setDaemon(true);//把t设置为后台线程
t.start();//启动线程
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("Main end");
}
}
友友们可以把这个代码贴到自己的编译器上面,看看有没有 setDaemon 的区别。
案例效果如下:
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。