【Java EE】多线程-初阶 认识线程(Thread)
多线程-初阶
- 1. 认识线程(Thread)
- 1.1 概念
- 1) 线程是什么
- 2) 为啥要有线程
- 3) 进程和线程的区别
- 4) Java 的线程 和 操作系统线程 的关系
- 1.2 第⼀个多线程程序
- 1.3 创建线程
- 1.4 多线程的优势-增加运⾏速度
本节⽬标
• 认识多线程
• 掌握多线程程序的编写
• 掌握多线程的状态
• 掌握什么是线程不安全及解决思路
• 掌握 synchronized、volatile 关键字
1. 认识线程(Thread)
结论:
进程在进行频繁创建和销毁的时候,开销比较大.(开销主要体现在 资源的申请 和 释放上)
线程 就是解决上述问题方案
线程(Thread)也可以称为"轻量级进程”,在进程的基础上,做出了改进。保持了独立调度执行,这样的"并发支持",同时省去"分配资源”"释放资源"带来的额外开销。
线程是怎么做到的呢?
前面介绍了会使用 PCB 来描述一个进程,现在,也使用 PCB 来描述一个线程
也不是随便搞两个线程,就能资源共享,把能够资源共享的这些线程,分成组,称为"线程组
再换句话讲,线程组,也就是 进程的一部分~
当引入的线程,达到一定数量之后,再继续尝试引入新的线程,就没有办法提升了~
当线程数量太多的时候,线程之间就会相互竞争cpu 的资源了(CPU 核心数是有限的),非但不会提高效率,反而还会增加调度的开销。
多线程,还有一个重要的问题,线程之间,可能会打架! 线程之间起了冲突,就可能会导致代码中出现一些逻辑上的错误.
线程安全问题[重点,难点]
多线程这种方式,不太好驾驭.主要还是因为这个东西,有一定的复杂程度~(后面细说)
多线程还有一个问题,共享资源,也会有副作用.
一个线程如果抛出异常,并且没有处理好,就可能会导致整个进程被终止(其他线程也就无了),如果及时捕获到,处理掉,也不一定导致进程终止~
多线程编程的值得关注的难点~ 一个线程出问题,会影响到别的线程。相比之下,进程和进程之间,独立性更好, 一个进程挂了,一般不会影响到其他进程。
上述讨论的 **线程的基本特点(进程和线程的区别)**非常经典,非常高频的面试题
1.1 概念
1) 线程是什么
⼀个线程就是⼀个 “执⾏流”. 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 “同时” 执⾏着多份代码.
还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业务。我们进⼀步设想如下场景:
⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。
如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。
此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。
2) 为啥要有线程
⾸先, “并发编程” 成为 “刚需”.
• 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU资源.
• 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.
• 创建线程⽐创建进程更快.
• 销毁线程⽐销毁进程更快.
• 调度线程⽐调度进程更快.
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
关于线程池我们后⾯再介绍. 关于协程的话题我们此处暂时不做过多讨论.
3) 进程和线程的区别
• 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
⽐如之前的多进程例⼦中,每个客⼾来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则钱不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。
• 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
• ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
临时小结:
上述讨论的 线程的基本特点(进程和线程的区别)非常经典,非常高频的面试题(操作系统这一类问题中,出场频率最高的问题,没有之一!!!)
这种 经典面试题,给出的回答都不要刻意去背。还是要写博客,用自己的话来总结表述。
面试的时候,面试官非常讨厌同学"背”
4) Java 的线程 和 操作系统线程 的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API (Application Programming Interface 应用程序编程接口)进⾏了进⼀步的抽象和封装.
1.2 第⼀个多线程程序
感受多线程程序和普通程序的区别:
• 每个线程都是⼀个独⽴的执⾏流
• 多个线程之间是 “并发” 执⾏的.
class MyThread extends Thread {@Overridepublic void run() {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class ThreadDemo {public static void main(String[] args) {Thread t = new MyThread();t.start();while (true) {System.out.println("hello main");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
看起来好像没啥区别~当引入多线程之后,代码中就可以同时具备多个执行流了!!!
1.
2.
3.
Thread 父类中,本身有一个 run 方法,程序员编写自己的逻辑,替代自身的 run.
@override
如果不写这个注解,也能否完成方法重写,那为啥还要写这个注解???
这个是检查语法的,语法中有很多的机制,是方便让编译器,对咱们的代码进行自动检查的,人是非常不靠谱的!!机器靠谱!!
怕就怕,你想要重写,结果参数写错了,没有构成重写,如果不报错, 就不容易发现
4.
t.start();
调用 操作系统 提供的"创建线程"api,在内核中创建对应 pcb, 并且把 pcb 加入到链表中进一步的系统调度到这个线程了之后,就会执行上述 run 方法中的逻辑!
start 创建了一个新的线程,多了一个执行流,能够干活~~(这个代码就可以"一心两用"同时做两件事)
调用 Thread 中自带的 start 方法,才会真正调用系统 api,在系统内核中创建出线程,线程就会执行上面写好的 run 方法了.虽然没有手动调用 run, 但是 run 还是执行了
注意!!!
当有多个线程的时候,这些线程执行的先后顺序,是不确定的!!!
这一点,因为 操作系统内核中,有一个"调度器" 模块,这个模块的实现方式,是一种类似于"随机调度" 效果
sleep
抛出try throw异常的快捷键 alt + enter 是 quick fix 功能
很多开发工具都有,不局限于 java
如果不抛出异常:
为啥上面的 sleep 只有一个 try catch 选项,不能 throws
第二个 sleep 就可以 throws ???
实际开发中,一般不会这么搞~
实际开发中,异常的处理方式:
通过打印的方式,可以看到两个执行流,还可以借助第三方工具, 查看多个线程的详细情况(只是针对 Java 进程来说)
使⽤ jconsole 命令观察线程
通过 java 提供的工具,更清楚的看到代码中的线程
jdk 中包含了 jconsole 工具
1.3 创建线程
• ⽅法1 继承 Thread 类(创建子类, 继承 Thread, 重写 run, 通过 start 启动线程)
1.继承 Thread 来创建⼀个线程类.
class MyThread extends Thread {@Overridepublic void run() {System.out.println("这⾥是线程运⾏的代码");} }
2.创建 MyThread 类的实例
MyThread t = new MyThread();
3.调⽤ start ⽅法启动线程
t.start(); // 线程开始运⾏
直接继承 Thread,执行的任务本身,和 Thread(线程) 这个概念是耦合在一起的~
写代码的时候,要考虑降低代码的耦合
• ⽅法2 实现 Runnable 接口(创建子类, 实现 Runnable, 重写 run, 搭配 Thread 的实例, 进行 start)
引入 Runnable 就是为了 解耦合,未来如果要更换成其他的方式来执行这些任务,改动成本比较低~
1.实现 Runnable 接⼝
class MyRunnable implements Runnable { @Override public void run() {System.out.println("这⾥是线程运⾏的代码"); } }
2.创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
3.调⽤ start ⽅法
t.start(); // 线程开始运⾏
Runnable 的作用,是描述了一个"任务"。这个任务 和 具体的执行机制无关(通过线程的方式执行,还是通过其他的方式执行)run 也就是要执行的任务内容本身了
通过 Runnable 表示线程要完成的任务 耦合更低。基于这种写法,更好的解耦合
写了一个项目,有很多代码,有很多文件,也有很多类,很多逻辑。把有关联的各种代码,放到一起.
只要和某个功能逻辑相关的东西,都在这一块 高内聚。如果某个功能的代码,这一块,那一块 低内聚
高内聚(一个模块之内,有关联的东西放一起)
低耦合(模块之间,依赖尽量小,影响尽量小)
对⽐上⾯两种⽅法:
第一种写法:是 Thread 自己记录我要干啥.自己记下来自己的作业
第二种写法:通过 Runnable 记录作业是啥.Thread 负责执行.别人给你把作业记下来了,然后 Thread 负责执行
继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤Thread.currentThread()
其他变形
本质上就是方法1和2,但是换一个写法,使用 匿名内部类 来实现
• 匿名内部类创建 Thread ⼦类对象(创建子类,继承 Thread, 匿名内部类)
本质上就是方法一使用匿名内部类
// 使⽤匿名类创建 Thread ⼦类对象 Thread t = new Thread() {@Overridepublic void run() {System.out.println("使⽤匿名类创建 Thread ⼦类对象");} }
这样就可以少定义一些类了
一般如果某个代码是"一次性”,就可以使用匿名内部类的写法
• 匿名内部类创建 Runnable ⼦类对象(创建子类,实现 Runnable, 匿名内部类)
// 使⽤匿名类创建 Runnable ⼦类对象 Thread t = new Thread(new Runnable() { @Override public void run() {System.out.println("使⽤匿名类创建 Runnable ⼦类对象"); } });
使用 Runnable,任务和线程概念是分离的
匿名内部类,写法非常常见的!!!
这里最主要的目的是描述这个方法(设置回调函数)。方法不能脱离类, 单独存在。这就导致为了设置回调函数,不得不套上一层类了
上述几种方法,都不常用~因此引入了 lambda 表达式(匿名函数/方法)
• lambda 表达式创建 Runnable ⼦类对象(推荐常用)
第五种写法,针对三和四进一步改进,引入lambda 表达式
// 使⽤ lambda 表达式创建 Runnab` le ⼦类对象 Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象")); Thread t4 = new Thread(() -> { System.out.println("使⽤匿名类创建 Thread ⼦类对象"); });
java 语法开了个特殊的口子:函数式接口属于 lambda 背后的实现,相当于 java 在没破坏原有的规则的基础上给了 lambda 一个合法性解释(方法不能脱离类, 单独存在)函数式接口” () -> {} 创建了一个匿名的函数式接口的子类,并且创建出对应的实例,并且重写了里面的方法(编译器在背后做的事情)
这个写法相当于 实现 Runnable 重写 run
lambda 代替了 Runnable 的位置~
此处 Thread 这里要谈到 5 种写法, 都很常用,都要掌握~
上述 5 种写法,都是等价的. 都是可以相互转换的。
本质上, 都是!
1)要把线程执行的任务内容表示出来.
2)通过 Thread 的 start 来创建/启动系统中的线程(Thread 对象和操作系统内核中的线程是一 一对应的关系)
这里也是一个常见面试题:Java 中创建线程都有哪些写法~
1.4 多线程的优势-增加运⾏速度
可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
• 使⽤ System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
• serial 串⾏的完成⼀系列运算. concurrency 使⽤两个线程并⾏的完成同样的运算.
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() {@Overridepublic 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);}
}
并发: 399.651856 毫秒
串⾏: 720.616911 毫秒