目录
1、什么是线程
一个线程就是一个 " 执行流 ", 每个线程之间都可以按照顺序执行自己的代码 .。 多个线程之间 " 同时 " 执行 着多份代码。
1.1、为什么要有线程
首先, "并发编程" 成为 "刚需"。单核 CPU 的发展遇到了瓶颈.,要想提高算力, 就需要多核CPU.。而并发编程能更充分利用多核 CPU 资源.,有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.。
其次, 虽然多进程也能实现并发编程, 但是线程比进程更轻量. 创建线程比创建进程更快. 销毁线程比销毁进程更快. 调度线程比调度进程更快
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
Thread myThread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000000; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
myThread.start();
//使用Runnable(),低耦合。
Thread myThread2 = new Thread(()-> {
for (int i = 0; i < 1000000; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
myThread2.start();
//lambda表达式书写
2、线程状态
线程状态有以下六种状态:
- New(新建)安排了工作, 还未开始行动
- Runnable(可运行)可工作的. 又可以分成正在工作中和即将开始工作
- Blocked(阻塞)表示排队等着其他事情
- Waiting(等待)表示排队等着其他事情
- Timed waiting(计时等待)表示排队等着其他事情
- Terminated(终止)工作完成了
2.1、可运行程序
一个线程开始运行后,它不是一直在运行,而是处于可运行状态(可能正在运行或不在运行,取决于系统调度)。
java中yiled()方法的作用是:让当前处于运行状态的线程退回到可运行状态,让出抢占资源的机会。
注意:调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间(也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的。
2.2、阻塞和等待线程
线程等待和线程阻塞的区别 两者都表示线程当前暂停执行的状态,而两者的区别,基本可以理解为:进入 waiting 状态是线程主动的,而进入 blocked 状态是被动的。
2.3、终止线程
线程会因为以下两种原因之一而终止:
- run方法的正常退出,线程自然终止;
- 因为一个没有捕获的异常而意外终止。
void join(); | 等待指定的线程终止。 |
void join(long millis); | 等待指定的线程终止或等待时间经过等待的毫秒数 |
Thread.State getState(); | 得到该线程的状态。 取值为:New(新建)、Runnable(可运行)、Blocked(阻塞)、Waiting(等待)、Timed waiting(计时等待)、Terminated(终止) |
void stop(); | 停止该线程。该方法已经废弃。 |
void suspend(); | 暂停该线程。该方法已经废弃。 |
void resume(); | 恢复线程,只能在调用suspend()之后使用。该方法已经废弃。 |
方法
|
说明
|
Thread()
|
创建线程对象
|
Thread(Runnable target)
|
使用
Runnable
对象创建线程对象
|
Thread(String name)
|
创建线程对象,并命名
|
Thread(Runnable target, String name)
|
使用
Runnable
对象创建线程对象,并命名
|
【了解】
Thread(ThreadGroup group,
Runnable target)
|
线程可以被用来分组管理,分好的组即为线程组,这
个目前我们了解即可
|
2.4、等待一个线程-join()
有的时候,当前线程需要等待另一个线程完成其任务后在开始本线程任务,我们可以使用jion()方法去等待线程结束;
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0 ; i < 5; i++) {
System.out.println("正在跑步");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread1.start();
thread1.join();
System.out.println("跑步结束");
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0 ; i < 5; i++) {
System.out.println("正在跑步");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread1.start();
thread1.join(2000);
System.out.println("跑步结束");
}
}
方法
|
说明
|
public void join()
|
等待线程结束
|
public void join(long millis)
|
等待线程结束,最多等
millis
毫秒
|
public void join(long millis, int nanos)
|
同理,但可以更高精度
|
2.5 获取当前线程引用
方法
| 说明 |
public static Thread currentThread();
|
返回当前线程对象的引用
|
public class Demo2 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
System.out.println(Thread.currentThread());
});
thread.start();
System.out.println(Thread.currentThread());
}
}
3、线程属性
3.1、中断线程
interrupt() 它基于「一个线程不应该由其他线程来强制中断或停止,而是应该由线程自己自行停止。」思想,是一个比较温柔的做法,它更类似一个标志位。其实作用不是中断线程,而是「通知线程应该中断了」,线程中断仅仅是置线程的中断状态位,不会停止线程。需要用户自己去监视线程的状态为并做处理。
public class Demo3 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("运行中+");
System.out.println(Thread.currentThread().isInterrupted());
}
});
thread.start();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
}
}
如果线程因为调用 wait/join/sleep 等方法而阻塞挂起,则以 InterruptedException 异常的形式通知,清除中断标志 ,当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法。可以选择忽略这个异常, 也可以跳出循环结束线程。
public class Demo3 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("运行中+");
System.out.println(Thread.currentThread().isInterrupted());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
}
}
public class Demo3 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("运行中+");
System.out.println(Thread.currentThread().isInterrupted());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().isInterrupted());
return;
}
}
});
thread.start();
try {
Thread.sleep(300);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
}
}
方法
|
说明
|
public void interrupt()
|
中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,
否则设置标志位
|
public static boolean
interrupted()
|
判断当前线程的中断标志位是否设置,调用后清除标志位
|
public boolean
isInterrupted()
|
判断对象关联的线程的标志位是否设置,调用后不清除标志位
|
3.2、守护线程
当 JVM 中不存在任何一个正在运行的非守护线程时,则 JVM 进程即会退出。通常来说,守护线程经常被用来执行一些后台任务,但是呢,你又希望在程序退出时,或者说 JVM 退出时,线程能够自动关闭,此时,守护线程是你的首选。
public class Demo4 {
public static void main(String[] args) {
Thread thread = new Thread(()->{
while(true){
System.out.println("当其他线程结束时,我才结束");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread1 = new Thread(()->{
for(int i = 0 ; i < 3; i++) {
System.out.println("我是非守护线程");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.setDaemon(true);
thread.start();
thread1.start();
}
}
void setDeamon(boolean isDeamon) | 将线程变为守护线程,必须要在线程启动前调用 |
4、多线程带来的的风险-线程安全
4.1、 观察线程不安全
我们使用两个线程对同一个变量继续操作,我们的期望是把count加到10000,但以下线程的执行结果并不会如我所愿。
class Count{
int count;
public Count(int count){
this.count = count;
}
void run(){
count++;
}
}
public class Demo5 {
public static void main(String[] args) {
Count con = new Count(0);
Thread thread1 = new Thread(()->{
for(int i = 0 ; i < 5000 ; i++){
con.run();
}
});
Thread thread2 = new Thread(()->{
for(int i = 0 ; i < 5000 ; i++){
con.run();
}
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(con.count);
}
}
4.2、线程安全的概念
4.3 线程不安全的原因
线程不安全的主要原因:
- 系统的随机调度,抢占式执行
- 多线程同时尝试修改同一变量
- 修改操作不是原子的
- 内存可见性引起的问题
- 指令重排序引起的问题
1. 从内存把数据读到 CPU2. 进行数据更新3. 把数据写回到 CPU
4.3.1、可见性
4.3.2、Java 内存模型 (JMM)
- 线程之间的共享变量存在主内存 (Main Memory).
- 每一个线程都有自己的 "工作内存" (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
- 为什么要整这么多内存?
- 为什么要这么麻烦的拷来拷去?
4.3.3、代码顺序性
- 1. 去前台取下 U 盘
- 2. 去教室写 10 分钟作业
- 3. 去前台取下快递
如果是在单线程情况下,JVM 、 CPU 指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问 题,可以少跑一次前台。这种叫做指令重排序。 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代 码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价 。
5、线程安全
5.1、synchronized 的特性
5.1.1、互斥
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
理解 "阻塞等待"针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁。
当我们给这个成员方法加上synchronized关键字时,操作系统在执行count++时就会完整地执行所有指令,最后的count结果也会如我们预期一样。
synchronized void run(){
count++;
}
5.1.2、刷新内存
synchronized 的工作过程 :
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized 也能保证内存可见性
5.1.3、可重入
- increase 和 increase2 两个方法都加了 synchronized, 此处的 synchronized 都是针对 this 当前对象加锁的.
- 在调用 increase2 的时候, 先加了一次锁, 执行到 increase 的时候, 又加了一次锁. (上个锁还没释放, 相当于连续加两次锁)
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
5.2 synchronized 使用示例
5.2.1 直接修饰普通方法:
public class SynchronizedDemo {
public synchronized void methond() {
}
}
5.2.2 修饰静态方法:
public class SynchronizedDemo {
public synchronized static void method() {
}
}
5.2.3 修饰代码块:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
6. volatile 关键字
6.1 volatile 能保证内存可见性
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
前面我们讨论内存可见性时说了 , 直接访问工作内存 ( 实际是 CPU 的寄存器或者 CPU 的缓存 ), 速度非常快, 但是可能出现数据不一致的情况.加上 volatile , 强制读写内存 . 速度是慢了 , 但是数据变的更准确了.
- 创建两个线程 t1 和 t2
- t1 中包含一个循环, 这个循环以 flag == 0 为循环条件.
- t2 中从键盘读入一个整数, 并把这个整数赋值给 flag.
- 预期当用户输入非 0 的值的时候, t1 线程结束.
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
t1 读的是自己工作内存中的内容 .当 t2 对 flag 变量进行修改 , 此时 t1 感知不到 flag 的变化 .
static class Counter {
public volatile int flag = 0;
}
// 执行效果
// 当用户输入非0值时, t1 线程循环能够立即结束.
6.2 volatile 不保证原子性
- 给 increase 方法去掉 synchronized
- 给 count 加上 volatile 关键字.
static class Counter {
volatile public int count = 0;
void increase() {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
final Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
- 去掉 flag 的 volatile
- 给 t1 的循环内部加上 synchronized, 并借助 counter 对象加锁.
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (true) {
synchronized (counter) {
if (counter.flag != 0) {
break;
}
}
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
7. wait 和 notify
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意 : wait, notify, notifyAll 都是 Object 类的方法 .
7.1 wait()方法
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 要搭配 synchronized 来使用 . 脱离 synchronized 使用 wait 会直接抛出异常 .
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出 InterruptedException 异常.
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
7.2 notify()方法
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
- 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 "先来后到")
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
- 创建 WaitTask 类, 对应一个线程, run 内部循环调用 wait.
- 创建 NotifyTask 类, 对应另一个线程, 在 run 内部调用一次 notify
- 注意, WaitTask 和 NotifyTask 内部持有同一个 Object locker. WaitTask 和 NotifyTask 要想配合 就需要搭配同一个 Object.
static class WaitTask implements Runnable {
private Object locker;
public WaitTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
while (true) {
try {
System.out.println("wait 开始");
locker.wait();
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
static class NotifyTask implements Runnable {
private Object locker;
public NotifyTask(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
Thread.sleep(1000);
t2.start();
}
7.3 notifyAll()方法
static class WaitTask implements Runnable {
// 代码不变
}
static class NotifyTask implements Runnable {
// 代码不变
}
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(new WaitTask(locker));
Thread t3 = new Thread(new WaitTask(locker));
Thread t4 = new Thread(new WaitTask(locker));
Thread t2 = new Thread(new NotifyTask(locker));
t1.start();
t3.start();
t4.start();
Thread.sleep(1000);
t2.start();
}
此时可以看到 , 调用 notify 只能唤醒一个线程 .
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notifyAll();
System.out.println("notify 结束");
}
}
此时可以看到 , 调用 notifyAll 能同时唤醒 3 个 wait 中的线程
7.4 wait 和 sleep 的对比
9. 多线程案例
9.1 单例模式
啥是设计模式 ?设计模式好比象棋中的 " 棋谱 ". 红方当头炮 , 黑方马来跳 . 针对红方的一些走法 , 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏 .软件开发中也有很多常见的 " 问题场景 ". 针对这些问题场景 , 大佬们总结出了一些固定的套路 . 按照 这个套路来实现代码, 也不会吃亏 .
这一点在很多场景上都需要 . 比如 JDBC 中的 DataSource 实例就只需要一个 .
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;
}
}
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法 , 就可能导致 创建出多个实例.一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了 ( 不再修改 instance 了 )
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 使用双重 if 判定, 降低锁竞争的频率.
- 给 instance 加上了 volatile.
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
理解双重 if 判定 / volatile:加锁 / 解锁是一件开销比较高的事情 . 而懒汉模式的线程不安全只是发生在首次创建实例的时候 . 因此后续使用的时候, 不必再进行加锁了 .外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了 .同时为了避免 " 内存可见性 " 导致读取的 instance 出现偏差 , 于是补充上 volatile .当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁 ,其中竞争成功的线程, 再完成创建实例的操作 .当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了 . 也就不会继续创建其他实例 .1) 有三个线程 , 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息 . 于是开始竞争同一把锁。2) 其中线程 1 率先获取到锁 , 此时线程 1 通过里层的 if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来。
3) 当线程 1 释放锁之后 , 线程 2 和 线程 3 也拿到锁 , 也通过里层的 if (instance == null) 来确认实例是否已经创建 , 发现实例已经创建出来了 , 就不再创建了。4) 后续的线程 , 不必加锁 , 直接就通过外层 if (instance == null) 就知道实例已经创建了 , 从而不再尝试获取锁了 . 降低了开销。
9.2 阻塞式队列
9.2.1 阻塞队列是什么
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
9.2.2 生产者消费者模型
比如在 " 秒杀 " 场景下 , 服务器同一时刻可能会收到大量的支付请求 . 如果直接处理这些支付请求 , 服务器可能扛不住( 每个支付请求的处理都需要比较复杂的流程 ).这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求 . 这样做可以有效进行 " 削峰 ", 防止服务器被突然到来的一波请求直接冲垮 .
比如过年一家人一起包饺子. 一般都是有明确分工 , 比如一个人负责擀饺子皮 , 其他人负责包 . 擀饺子皮的人就是 " 生产者 ", 包饺子的人就是 " 消费者 ".擀饺子皮的人不关心包饺子的人是谁( 能包就行 , 无论是手工包 , 借助工具 , 还是机器包 ), 包饺子的人也不关心擀饺子皮的人是谁( 有饺子皮就行 , 无论是用擀面杖擀的 , 还是拿罐头瓶擀 , 还是直接从超市买的).
9.2.3 标准库中的阻塞队列
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
- put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("生产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
- 通过 "循环队列" 的方式来实现.
- 使用 synchronized 进行加锁控制.
- put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
- take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
public class BlockingQueue {
private int[] items = new int[1000];
private volatile int size = 0;
private int head = 0;
private int tail = 0;
public void put(int value) throws InterruptedException {
synchronized (this) {
// 此处最好使用 while.
// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了
// 就只能继续等待
while (size == items.length) {
wait();
}
items[tail] = value;
tail = (tail + 1) % items.length;
size++;
notifyAll();
}
}
public int take() throws InterruptedException {
int ret = 0;
synchronized (this) {
while (size == 0) {
wait();
}
ret = items[head];
head = (head + 1) % items.length;
size--;
notifyAll();
}
return ret;
}
public synchronized int size() {
return size;
}
// 测试代码
public static void main(String[] args) throws InterruptedException {
BlockingQueue blockingQueue = new BlockingQueue();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
blockingQueue.put(random.nextInt(10000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "生产者");
producer.start();
customer.join();
producer.join();
}
}
9.3 定时器
9.3.1 定时器是什么
定时器是一种实际开发中非常常用的组件.比如网络通信中, 如果对方 500ms 内没有返回数据 , 则断开连接尝试重连 .比如一个 Map, 希望里面的某个 key 在 3s 之后过期 ( 自动删除 ).类似于这样的场景就需要用到定时器.
9.3.2 标准库中的定时器
- 标准库中提供了一个 Timer 类. Timer 类的核心方法为 schedule .
- schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
9.3.3 实现定时器
为啥要带优先级呢 ?因为阻塞队列中的任务都有各自的执行时刻 (delay). 最先执行的任务一定是 delay 最小的 . 使用带优先级的队列就可以高效的把这个 delay 最小的任务找出来 .
- 队列中的每个元素是一个 Task 对象.
- Task 中带有一个时间属性, 队首元素就是即将
- 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
/**
* 定时器的构成:
* 一个带优先级的阻塞队列
* 队列中的每个元素是一个 Task 对象.
* Task 中带有一个时间属性, 队首元素就是即将
* 同时有一个 worker 线程一直扫描队首元素, 看队首元素是否需要执行
*/
public class Timer {
static class Task implements Comparable<Task> {
private Runnable command;
private long time;
public Task(Runnable command, long time) {
this.command = command;
// time 中存的是绝对时间, 超过这个时间的任务就应该被执行
this.time = System.currentTimeMillis() + time;
}
public void run() {
command.run();
}
@Override
public int compareTo(Task o) {
// 谁的时间小谁排前面
return (int)(time - o.time);
}
}
// 核心结构
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue();
// 存在的意义是避免 worker 线程出现忙等的情况
private Object mailBox = new Object();
class Worker extends Thread{
@Override
public void run() {
while (true) {
try {
Task task = queue.take();
long curTime = System.currentTimeMillis();
if (task.time > curTime) {
// 时间还没到, 就把任务再塞回去
queue.put(task);
synchronized (mailBox) {
// 指定等待时间 wait
mailBox.wait(task.time - curTime);
}
} else {
// 时间到了, 可以执行任务
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
public Timer() {
// 启动 worker 线程
Worker worker = new Worker();
worker.start();
}
// schedule 原意为 "安排"
public void schedule(Runnable command, long after) {
Task task = new Task(command, after);
queue.offer(task);
synchronized (mailBox) {
mailBox.notify();
}
}
public static void main(String[] args) {
Timer timer = new Timer();
Runnable command = new Runnable() {
@Override
public void run() {
System.out.println("我来了");
timer.schedule(this, 3000);
}
};
timer.schedule(command, 3000);
}
}
9.4 线程池
9.4.1 线程池是什么
想象这么一个场景:在学校附近新开了一家快递店,老板很精明,想到一个与众不同的办法来经营。店里没有雇人,而是每次有业务来了,就现场找一名同学过来把快递送了,然后解雇同学。这个类比我们平时来一个任务,起一个线程进行处理的模式。很快老板发现问题来了,每次招聘 + 解雇同学的成本还是非常高的。老板还是很善于变通的,知道了为什么大家都要雇人了,所以指定了一个指标,公司业务人员会扩张到 3 个人,但还是随着 业务逐步雇人。于是再有业务来了,老板就看,如果现在公司还没 3 个人,就雇一个人去送快递,否则只是把业务放到一个本本上,等着 3 个快递人员空闲的时候去处理。这个就是我们要带 出的线程池的模式。
9.4.2 标准库中的线程池
- 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
- 返回值类型为 ExecutorService
- 通过 ExecutorService.submit 可以注册一个任务到线程池中.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
- newFixedThreadPool: 创建固定线程数的线程池
- newCachedThreadPool: 创建线程数目动态增长的线程池.
- newSingleThreadExecutor: 创建只包含单个线程的线程池.
- newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
Executors 本质上是 ThreadPoolExecutor 类的封装.
9.4.3 实现线程池
- 核心操作为 submit, 将任务加入线程池中
- 使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
- 使用一个 BlockingQueue 组织所有的任务
- 每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
- 指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增线程了.
class Worker extends Thread {
private LinkedBlockingQueue<Runnable> queue = null;
public Worker(LinkedBlockingQueue<Runnable> queue) {
super("worker");
this.queue = queue;
}
@Override
public void run() {
// try 必须放在 while 外头, 或者 while 里头应该影响不大
try {
while (!Thread.interrupted()) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
}
}
}
public class MyThreadPool {
private int maxWorkerCount = 10;
private LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue();
public void submit(Runnable command) {
if (workerList.size() < maxWorkerCount) {
// 当前 worker 数不足, 就继续创建 worker
Worker worker = new Worker(queue);
worker.start();
}
// 将任务添加到任务队列中
queue.put(command);
}
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool();
myThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("吃饭");
}
});
Thread.sleep(1000);
}
}
10. 总结-保证线程安全的思路
- 不需要写共享资源的模型
- 使用不可变对象
- 保证原子性
- 保证顺序性
- 保证可见性
11. 对比线程和进程
11.1 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
11.2 进程与线程的区别
- 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位。
- 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
- 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
- 线程的创建、切换及终止效率更高.