文章目录
- 本篇目标:
- 一、认识线程(Thread)
- 1.概念:
- 2.创建线程
- 二、Thread 类及常见方法
- 2.1 Thread 的常见构造方法
- 2.2 Thread 的几个常见属性
- 2.3 启动⼀个线程 - start()
- 2.4 中断⼀个线程
- 2.5 等待⼀个线程 - join()
- 2.6 获取当前线程引用
- 2.7 休眠当前线程 - sleep()
- 三、线程的状态
- 3.1 观察线程的所有状态
- 四、 多线程带来的的风险-线程安全 (重点)
- 4.1 线程安全的概念
- 4.2 线程不安全的原因
- 五、synchronized 关键字 - 监视器锁 monitor lock
- 5.1 synchronized 的特性
- 5.2 synchronized 使用示例
- 总结
本篇目标:
- 认识多线程
- 掌握多线程程序的编写
- 掌握多线程的状态
- 掌握什么是线程不安全及解决思路
- 掌握 synchronized关键字
提示:以下是本篇文章正文内容
一、认识线程(Thread)
1.概念:
1.1 线程是什么?
线程: ⼀个线程就是⼀个 “执行流”. 每个线程之间都可以按照顺序执行自己的代码. 多个线程之间 “同时” 执行着多份代码.
1.2 为什么要有线程?
首先, “并发编程” 成为 “刚需”.
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
- 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程
其次, 虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)
1.3 进程和线程的区别
- 进程是包含线程的. 每个进程至少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位。
- ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带走(整个进程崩溃).
1.4 Java的线程和操作系统线程的关系
- 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了⼀些 API 供用户使用(例如 Linux 的 pthread 库).
- Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进⼀步的抽象和封装
2.创建线程
方法一:继承 Thread 类
1.继承 Thread 来创建⼀个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
2.创建 MyThread 类的实例
MyThread t = new MyThread();
3.调用 start 方法启动线程
t.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(); // 线程开始运⾏
对比上面两种方法:
• 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
• 实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用Thread.currentThread()
其他变形:
- 匿名内部类创建 Thread 子类对象
// 使⽤匿名类创建 Thread ⼦类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Thread ⼦类对象");
}
};
- 匿名内部类创建 Runnable 子类对象
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
- lambda 表达式创建 Runnable 子类对象
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {
System.out.println("使⽤匿名类创建 Thread 子类对象");
});
二、Thread 类及常见方法
Thread 类是 JVM 用来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
2.1 Thread 的常见构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target, String name) | 使用Runnable 对象创建线程对象,并命名 |
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread 的几个常见属性
属性 | 获取方法 |
---|---|
ID | getld0) |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted( ) |
- ID 是线程的唯⼀标识,不同线程不会重复
- 名称是各种调试工具用到
- 状态表示线程当前所处的⼀个情况,下面我们会进⼀步说明
- 优先级高的线程理论上来说更容易被调度到
- 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有非后台线程结束后,才会结束运行。
- 是否存活,即简单的理解,为 run 方法是否运行结束了
- 线程的中断问题,下面我们进⼀步说明
2.3 启动⼀个线程 - start()
调用 start 方法, 才真的在操作系统的底层创建出⼀个线程。
2.4 中断⼀个线程
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
2.5 等待⼀个线程 - join()
有时,我们需要等待⼀个线程完成它的工作后,才能进行自己的下⼀步工作。
方法 | 说明 |
---|---|
public void join() | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等millis毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
2.6 获取当前线程引用
方法 | 说明 |
---|---|
public static Thread currentTkread() | 返回当前线程对象的引用 |
2.7 休眠当前线程 - sleep()
方法 | |
---|---|
public static void sleep(long millis) throws InterruptedException | |
public static void sleep(long millis, int nanos)throws InterruptedException |
三、线程的状态
3.1 观察线程的所有状态
线程的状态是⼀个枚举类型 Thread.State
- NEW: 安排了工作, 还未开始行动
- RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.
- BLOCKED: 这几个都表示排队等着其他事情
- WAITING: 这几个都表示排队等着其他事情
- TIMED_WAITING: 这几个都表示排队等着其他事情
- TERMINATED: 工作完成了
四、 多线程带来的的风险-线程安全 (重点)
4.1 线程安全的概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
4.2 线程不安全的原因
- **线程调度是随机的 **,这是线程安全问题的 罪魁祸首, 随机调度使⼀个程序在多线程环境下, 执行顺序存在很多的变数,程序猿必须保证在任意执行顺序下 , 代码都能正常工作。
- 修改共享数据,多个线程修改同⼀个变量。
- 原子性
- 可见性
- 指令重排序
【拓展】:Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到⼀致的并发效果。
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每⼀个线程都有自己的 “工作内存” (Working Memory) .
- 当线程要读取⼀个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改⼀个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同⼀个共享变量的 “副本”. 此时修改线程1 的工作内存中的值, 线程2 的工作内存不⼀定会及时变化。
此时引柚柚们可能会有这些问题:
- 为啥要整这么多内存?
- 为啥要这么麻烦的拷来拷去?
-
为啥整这么多内存?
实际并没有这么多 “内存”. 这只是 Java 规范中的⼀个术语, 是属于 “抽象” 的叫法.
所谓的 “主内存” 才是真正硬件角度的 “内存”. 而所谓的 “工作内存”, 则是指 CPU 的寄存器和高速缓存。 -
为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级,也就是几千倍, 上万倍).
那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??
答案就是⼀个字: 贵
五、synchronized 关键字 - 监视器锁 monitor lock
5.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同⼀个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
注意: - 上⼀个线程解锁之后, 下⼀个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的⼀部分工作.
- 假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不⼀定就能获取到锁,而是和 C 重新竞争, 并不遵守先来后到的规则.
2) 可重入
synchronized 同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。
5.2 synchronized 使用示例
synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配⼀个具体的对象来使用。
1) 修饰代码块: 明确指定锁哪个对象。
锁任意对象
public class SynchronizedDemo {
private Object locker = new Object();
public void method() {
synchronized (locker) {
}
}
}
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待
总结
多线程几乎是面试必问题,柚柚们一定要好好理解喔!!!