1. 认识线程(Thread)
1.1 概念
1) 线程是什么
⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.线程,就是程序里一条独立的 "执行流水线"。一个程序可以同时跑 多条流水线,这就是多线程。
2) 为啥要有线程
⾸先, "并发编程" 成为 "刚需"
• 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU
资源.
• 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量
• 创建线程⽐创建进程更快.
• 销毁线程⽐销毁进程更快.
• 调度线程⽐调度进程更快.
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池和 "协程"
3) 进程和线程的区别
• 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。但是不能没有线程
• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间(内存,硬盘,网络带宽.......)
• 进程是系统分配资源 的基本单位,线程是系统调度执行 的基本单位。
• 多个线程之间,可能会相互影响。如果⼀个线程挂了, 就会把同进程内的其他线程⼀起带⾛(整个进程崩溃)
• 多个进程之间,一般不会相互影响。一个进程崩了,不会影响到其他进程。(进程的隔离性)
•线程是当下是实现并发编程的主流方式。通过多线程,就可以充分利用 多核CPU 。但是,也不是线程数目越多越好。线程数目达到一定量后,把多个核心都利用拆分之后,在增加线程,就会无法提高效率,也有可以会影响效率。(线程调度也是有开销的)
4) Java 的线程 和 操作系统线程 的关系
• 线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾使⽤。
• java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装.
1.2 第⼀个多线程程序
• 每个线程都是⼀个独⽴的执⾏流
• 多个线程之间是 "并发" 执⾏的.
import java.util.Random;
public class test {
private static class MyThread extends Thread {
@Override
public void run() {
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
// 随机停⽌运⾏ 0-9 秒
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
MyThread t1 = new MyThread();
t1.start();
Random random = new Random();
while (true) {
// 打印线程名称
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(random.nextInt(10));
} catch (InterruptedException e) {
// 随机停⽌运⾏ 0-9 秒
e.printStackTrace();
}
}
}
}
1.3使⽤ jconsole 命令观察线程

1.4 创建线程
⽅法1 继承 Thread 类
继承 Thread 来创建⼀个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
public class demo1 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}

⽅法2 实现 Runnable 接⼝
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这⾥是线程运⾏的代码");
}
}
public class demo1 {
public static void main(String[] args) {
new Thread(new MyRunnable()).start();
}
}
其他变形
• 匿名内部类创建 Thread ⼦类对象
public class demo1 {
public static void main(String[] args) {
// 使用匿名类创建 Thread 子类对象
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
// 启动线程
thread.start();
}
}
• 匿名内部类创建 Runnable ⼦类对象
public class demo1 {
public static void main(String[] args) {
// 使⽤匿名类创建 Runnable ⼦类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使⽤匿名类创建 Runnable ⼦类对象");
}
});
// 启动线程
t2.start();
}
}
• lambda 表达式创建 Runnable ⼦类对象
public class demo1 {
public static void main(String[] args) {
// 使⽤ lambda 表达式创建 Runnable ⼦类对象
Thread t3 = new Thread(() -> System.out.println("使⽤匿名类创建 Thread ⼦类对象"));
Thread t4 = new Thread(() -> {System.out.println("使⽤匿名类创建 Thread ⼦类对象");
});
t3.start();
t4.start();
}
}
1.5 多线程的优势-增加运⾏速度
可以观察多线程在⼀些场合下是可以提⾼程序的整体运⾏效率的。
2. Thread 类及常⻅⽅法
2.1 Thread 的常⻅构造⽅法

Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
2.2 Thread 的⼏个常⻅属性

•ID 是线程的唯⼀标识,不同线程不会重复
• 名称是身份的标识
• 状态表⽰线程当前所处的⼀个情况
• 优先级⾼的线程理论上来说更容易被调度到
• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
• 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
• 前台线程:如果某个线程在执行的过程中,能够阻止进程结束,这个线程就是"前台线程"
• 后台线程:如果某个线程在执行的过程中,不能够阻止进程结束,(虽然线程在执行着,但是进程要结束了,此时这个线程也要随之结束)这个线程就是"后台线程"
2.3 启动⼀个线程 - start()
• 覆写 run ⽅法是提供给线程要做的事情的指令清单
• 线程对象可以认为是把 李四、王五叫过来了
•调⽤ start() ⽅法,就是喊⼀声:"⾏动起来!",线程才真正独⽴去执⾏了。

调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程.
💡经典面试题:start () 和 run () 方法的区别
start () 是启动线程,run () 只是普通方法调用;start () 会真正开启新线程,run () 不会。
-
Thread.start()
- 属于
Thread类的方法 - 真正启动一个新线程 :会让线程进入就绪状态 ,等待 CPU 调度,自动 调用线程的
run()方法 - 是多线程的正确启动方式
- 属于
-
Thread.run()
- 属于
Runnable接口的方法 - 只是一个普通的成员方法
- 直接调用
run(),不会创建新线程 ,代码在当前调用线程(比如 main 线程) 中同步执行
- 属于
| 对比维度 | start () 方法 | run () 方法 |
|---|---|---|
| 是否创建新线程 | ✅ 会创建新线程 | ❌ 不会创建新线程 |
| 执行线程 | 新的独立线程 | 当前调用线程(如 main 线程) |
| 执行方式 | 异步执行(和主线程同时跑) | 同步执行(必须等 run 执行完才往下走) |
| 调用次数 | 只能调用1 次(线程生命周期限制) | 可以调用多次(普通方法) |
| 本质 | 启动线程的入口 | 业务逻辑的载体 |
2.4 中断⼀个线程
2.4-1: 使⽤⾃定义的变量来作为标志位
public class demo2 {
private static class MyRunnable implements Runnable {
public volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了⼤事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
target.isQuit = true;
}
}
2.4-2: 使⽤ Thread.interrupted()

public class demo2 {
private static class MyRunnable implements Runnable {
@Override
public void run() {
// 两种⽅法均可以
while (!Thread.interrupted()) {
//while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()
+ ": 别管我,我忙着转账呢!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().getName()
+ ": 有内⻤,终⽌交易!");
// 注意此处的 break
break;
}
}
System.out.println(Thread.currentThread().getName()
+ ": 啊!险些误了⼤事");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
System.out.println(Thread.currentThread().getName()
+ ": 让李四开始转账。");
thread.start();
Thread.sleep(10 * 1000);
System.out.println(Thread.currentThread().getName()
+ ": ⽼板来电话了,得赶紧通知李四对⽅是个骗⼦!");
thread.interrupt();
}
}
thread 收到通知的⽅式有两种:
- 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志
◦ 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽
略这个异常, 也可以跳出循环结束线程. - 否则,只是内部的⼀个中断标志被设置,thread 可以通过
◦ Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到。
2.5 等待⼀个线程 - join()

操作系统,针对多个线程的执行,是一个"随机调度,抢占式执行"的过程。
线程等待就是确定两个线程的"结束顺序"
有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作例如,王五只有等李四工作完成工作之后才能工作,这时我们需要⼀个⽅法明确等待线程的结束。
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Runnable target = () -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName()
+ ": 我还在⼯作!");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我结束了!");
};
Thread thread1 = new Thread(target, "李四");
Thread thread2 = new Thread(target, "王五");
System.out.println("先让李四开始⼯作");
thread1.start();
thread1.join();
System.out.println("李四⼯作结束了,让王五开始⼯作");
thread2.start();
thread2.join();
System.out.println("王五⼯作结束了");
}
}
2.6 获取当前线程引⽤
| 方法 | 说明 |
|---|---|
| public static Thread currentThread() | 返回当前线程对象的引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7 休眠当前线程
线程的调度是不可控的,所以,这个⽅法只能保证实际休眠时间是⼤于等于参数设置的休眠时间的。

3. 线程的状态
3.1 观察线程的所有状态
线程的状态是⼀个枚举类型 Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
• NEW: 安排了⼯作, 还未开始⾏动(new)
• RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.(runnable)
• BLOCKED: 这⼏个都表⽰排队等着其他事情(blocked)
• WAITING: 这⼏个都表⽰排队等着其他事情(waiting)
• TIMED_WAITING: 这⼏个都表⽰排队等着其他事情(timed-waiting)
• TERMINATED: ⼯作完成了.(terminated)
4. 多线程带来的的⻛险-线程安全 (重点)
4.1 线程安全的概念
如果多线程环境下代码运⾏的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
4.2 观察线程不安全
public class demo4 {
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来的 count 就是个 0 t1.join();
t2.join();
// 预期结果应该是 10w System.out.println("count: " + count);
}
}
4.3 线程不安全的原因
线程调度是随机的 这是线程安全问题的 罪魁祸⾸
随机调度使⼀个程序在多线程环境下, 执⾏顺序存在很多的变数
原因1-->修改共享数据
多个线程修改同⼀个变量
原因2-->原子性
如果⼀个线程正在对⼀个变量操作,中途其他线程插⼊进来了,如果这个操作被打断了,结果就可能是错误的。
原因3-->可⻅性
可⻅性指, ⼀个线程对共享变量值的修改,能够及时地被其他线程看到
原因4-->指令重排序
CPU / 编译器为了提高执行效率,会在不影响 单线程执行结果的前提下,打乱代码的实际执行顺序 但在多线程下,这种重排会导致逻辑错误、数据不一致。
4.4 解决之前的线程不安全问题
public class demo5 {
// 此处定义⼀个 int 类型的变量
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
Thread t2 = new Thread(() -> {
// 对 count 变量进⾏⾃增 5w 次
for (int i = 0; i < 50000; i++) {
synchronized (locker) {
count++;
}
}
});
t1.start();
t2.start();
// 如果没有这俩 join, 肯定不⾏的. 线程还没⾃增完, 就开始打印了. 很可能打印出来count 就是个 0 t1.join();
t2.join();
// 预期结果应该是 10w System.out.println("count: " + count);
}
}

5. synchronized 关键字 - 监视器锁 monitor lock
5.1 synchronized 的特性
1) 互斥
synchronized 会起到互斥效果 , 某个线程执⾏到某个对象的 synchronized 中时, 其他线程如果也执⾏到同⼀个对象 synchronized 就会阻塞等待.
• 进⼊ synchronized 修饰的代码块, 相当于 加锁
• 退出 synchronized 修饰的代码块, 相当于 解锁

2) 可重⼊
synchronized 同步块对同⼀条线程来说是可重⼊的,不会出现⾃⼰把⾃⼰锁死的问题;
⼀个线程没有释放锁, 然后⼜尝试再次加锁.
// 第⼀次加锁, 加锁成功
lock();
// 第⼆次加锁, 锁已经被占⽤, 阻塞等待.
lock();

这样的锁称为**不可重⼊锁. Java 中的 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 锁的是什么. 两个线程竞争同⼀把锁, 才会产⽣阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产⽣竞争.
💡Java 死锁
一、死锁一句话定义
两个或多个线程,互相持有对方需要的锁,谁都不释放,导致程序永久卡住。
就像:
- 你拿着我要的钥匙
- 我拿着你要的钥匙
- 我们都不肯松手
→ 卡死,谁都动不了
二、死锁的 4 个必要条件(必须同时满足)
只要破坏任意一条,死锁就不会发生。
-
互斥:资源只能被一个线程占用
-
请求与保持:线程拿着自己的锁,又去申请别人的锁
-
不可剥夺:锁只能自己释放,不能被抢走
-
循环等待 :线程形成一个环形等待链
- A 等 B
- B 等 A
public class demo6 {
public static void main(String[] args) {
Object lockA = new Object();
Object lockB = new Object();// 线程1:先拿A,再拿B new Thread(() -> { synchronized (lockA) { System.out.println("线程1持有 lockA"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockB) { System.out.println("线程1持有 lockB"); } } }).start(); // 线程2:先拿B,再拿A new Thread(() -> { synchronized (lockB) { System.out.println("线程2持有 lockB"); try { Thread.sleep(100); } catch (Exception e) {} synchronized (lockA) { System.out.println("线程2持有 lockA"); } } }).start(); }}
运行结果:程序直接卡住,不动了 这就是死锁。
三、死锁的特点
- 程序不报错
- 不退出
- CPU 占用不高
- 线程全部阻塞
- 日志不动,接口不返回,服务假死
四、怎么避免死锁?(最实用)
破坏 4 个条件中的任意一个即可!
最常用、最简单的 3 种方案:
1. 统一锁的获取顺序(最有效)
所有线程都按照 lockA → lockB 的顺序拿锁
不要一个 A→B,一个 B→A
2. 使用定时锁(tryLock)
拿不到锁就超时放弃,不一直死等
3. 避免嵌套锁
不要在一个锁里面再拿另一个锁
5.3 Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的 . 这些类可能会涉及到多线程修改共享数据, ⼜没有任何加锁措
施.
• ArrayList • LinkedList • HashMap
• TreeMap • HashSet • TreeSet
• StringBuilder
但是还有⼀些是线程安全的. 使⽤了⼀些锁机制来控制.
• String • StringBuffer
6. volatile 关键字
volatile 能保证内存可⻅性
代码在写⼊ volatile 修饰的变量的时候,
• 改变线程⼯作内存中volatile变量副本的值
• 将改变后的副本的值从⼯作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候,
• 从主内存中读取volatile变量的最新值到线程的⼯作内存中
• 从⼯作内存中读取volatile变量的副本
volatile 不保证原⼦性
volatile 和 synchronized 有着本质的区别. synchronized 能够保证原⼦性, volatile 保证的是内存可⻅性.
-
单线程下,编译器 / JVM 的优化是安全的
单线程中,编译器和 JVM 会基于
as-if-serial规则做优化(比如指令重排、缓存优化),保证程序执行结果和代码顺序执行的结果完全一致,不会出问题。 -
多线程下,优化会出现 "误判",导致内存可见性问题
多线程场景下,JVM / 编译器的优化是基于 "单线程视角" 做的,无法感知其他线程的存在。比如:
-
把变量的值缓存到寄存器 / 本地内存,不及时写回主内存
-
对指令进行重排,导致其他线程看到的执行顺序错乱
这些优化在单线程下没问题,但多线程下就会导致一个线程的修改,另一个线程看不到,也就是内存可见性问题。
-
-
为什么要做这些优化?
核心目的是提升程序执行效率:
- 降低程序员写代码的门槛,即使代码写得不够高效,编译器也能自动优化
- 减少内存访问、提升 CPU 流水线利用率,让程序跑得更快
7. wait 和 notify
完成这个协调⼯作, 主要涉及到三个⽅法
• wait() / wait(long timeout): 让当前线程进⼊等待状态.
• notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的⽅法
7.1 wait()⽅法
wait 做的事情:
• 使当前执⾏代码的线程进⾏等待. (把线程放到等待队列中)
• 释放当前的锁
• 满⾜⼀定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使⽤. 脱离 synchronized 使⽤ wait 会直接抛出异常.
wait 结束等待的条件:
• 其他线程调⽤该对象的 notify ⽅法.
• wait 等待时间超时 (wait ⽅法提供⼀个带有 timeout 参数的版本, 来指定等待时间).
• 其他线程调⽤该等待线程的 interrupted ⽅法, 导致 wait 抛出 InterruptedException 异常.
public class demo7 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait(1000);
System.out.println("等待结束");
}
}
}
7.2 notify()⽅法
notify ⽅法是唤醒等待的线程.
• ⽅法notify()也要在同步⽅法或同步块中调⽤,该⽅法是⽤来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
• 如果有多个线程等待,则有线程调度器随机挑选出⼀个呈 wait 状态的线程。(并没有 "先来后到")
• 在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏
完,也就是退出同步代码块之后才会释放对象锁
7.3 notifyAll()⽅法
notify⽅法只是唤醒某⼀个等待线程. 使⽤notifyAll⽅法可以⼀次唤醒所有的等待线程
💡理解 notify 和 notifyAll
notify 只唤醒等待队列中的⼀个线程. 其他线程还是乖乖等着

notifyAll ⼀下全都唤醒, 需要这些线程重新竞争锁

7.4 wait 和 sleep 的对⽐(⾯试题)
其实理论上 wait 和 sleep 完全是没有可⽐性的,因为⼀个是⽤于线程之间的通信的 ,⼀个是让线程阻塞⼀段时间,
唯⼀的相同点就是都可以让线程放弃执⾏⼀段时间.
总结一下:
- wait 需要搭配 synchronized 使⽤. sleep 不需要
- wait 是 Object 的⽅法 sleep 是 Thread 的静态⽅法.
- 使用wait目的是 为了提前唤醒,sleep就是固定时段的阻塞,不涉及唤醒
- wait默认的是 死等。sleep和锁无关。
8. 多线程案例
8.1 单例模式
单例模式能保证某个类在程序中只存在唯⼀⼀份实例, ⽽不会创建出多个实例
单例模式具体的实现⽅式有很多. 最常⻅的是 "饿汉" 和 "懒汉" 两种
饿汉模式
类加载的同时, 创建实例
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
懒汉模式-单线程版
类加载的时候不创建实例. 第⼀次使⽤的时候才创建实例.懒汉模式的实现是线程不安全的.
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式-多线程版
加上 synchronized 可以改善这⾥的线程安全问题
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
8.2 阻塞队列
1.阻塞队列是什么
阻塞队列是⼀种特殊的队列. 也遵守 "先进先出" 的原则
阻塞队列能是⼀种线程安全的数据结构, 并且具有以下特性:
• 当队列满的时候, 继续⼊队列就会阻塞, 直到有其他线程从队列中取⾛元素.
• 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插⼊元素.
阻塞队列的⼀个典型应⽤场景就是 "⽣产者消费者模型". 这是⼀种⾮常典型的开发模型.
2.⽣产者消费者模型

- 阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒. (削峰填⾕)
- 阻塞队列也能使⽣产者和消费者之间 解耦.
3.阻塞队列实现
• 通过 "循环队列" 的⽅式来实现.
• 使⽤ synchronized 进⾏加锁控制.
• put 插⼊元素的时候, 判定如果队列满了, 就进⾏ wait. (注意, 要在循环中进⾏ wait. 被唤醒时不⼀定队列就不满了, 因为同时可能是唤醒了多个线程).
• take 取出元素的时候, 判定如果队列为空, 就进⾏ wait. (也是循环 wait)
8.3 定时器
1.定时器是什么
定时器也是软件开发中的⼀个重要组件. 类似于⼀个 "闹钟". 达到⼀个设定的时间之后, 就执⾏某个指定好的代码
2.标准库中的定时器
• 标准库中提供了⼀个 Timer 类. Timer 类的核⼼⽅法为 schedule .
• schedule 包含两个参数. 第⼀个参数指定即将要执⾏的任务代码, 第⼆个参数指定多⻓时间之后
执⾏ (单位为毫秒).
3.实现定时器
• ⼀个带优先级队列(不要使⽤ PriorityBlockingQueue, 容易死锁!)
• 队列中的每个元素是⼀个 Task 对象.
• Task 中带有⼀个时间属性, 队⾸元素就是即将要执⾏的任务
• 同时有⼀个 worker 线程⼀直扫描队⾸元素, 看队⾸元素是否需要执⾏
8.4 线程池
线程池最⼤的好处就是减少每次启动、销毁线程的损耗。
标准库中的线程池
• 使⽤ Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
• 返回值类型为 ExecutorService
• 通过 ExecutorService.submit 可以注册⼀个任务到线程池中.
实现线程池
• 核⼼操作为 submit, 将任务加⼊线程池中
• 使⽤ Worker 类描述⼀个⼯作线程. 使⽤ Runnable 描述⼀个任务.
• 使⽤⼀个 BlockingQueue 组织所有的任务
• 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执⾏.
• 指定⼀下线程池中的最⼤线程数 maxWorkerCount; 当当前线程数超过这个最⼤值时, 就不再新增
线程了.
9. 对⽐线程和进程
优点
- 创建⼀个新线程的代价要⽐创建⼀个新进程⼩得多
- 与进程之间的切换相⽐,线程之间的切换需要操作系统做的⼯作要少很多
- 线程占⽤的资源要⽐进程少很多
- 能充分利⽤多处理器的可并⾏数量
- 在等待慢速I/O操作结束的同时,程序可执⾏其他的计算任务
- 计算密集型应⽤,为了能在多处理器系统上运⾏,将计算分解到多个线程中实现
- I/O密集型应⽤,为了提⾼性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
区别
- 进程是系统进⾏资源分配和调度的⼀个独⽴单位,线程是程序执⾏的最⼩单位。
- 进程有⾃⼰的内存地址空间,线程只独享指令流执⾏的必要资源,如寄存器和栈。
- 由于同⼀进程的各线程间共享内存和⽂件资源,可以不通过内核进⾏直接通信。
- 线程的创建、切换及终⽌效率更⾼。