博客主页:敲代码的布莱恩特
欢迎点赞 收藏 ⭐留言 欢迎讨论!
本文由 【敲代码的布莱恩特】 原创,首发于 CSDN
由于博主是在学小白一枚,难免会有错误,有任何问题欢迎评论区留言指出,感激不尽!✨
精品专栏(不定时更新)【JavaSE】 【Java数据结构】【LeetCode】
所谓的"线程",可以理解成轻量级"进程",也是一种实现并发编程的方式
如果把一个进程,想象成是一个工厂,线程就是工厂中的若干个流水线
可以实现并发编程
线程比进程更轻量
进程是包含线程的. 每个进程至少有一个线程存在,即主线程
进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
同一个进程的多个线程之间共享的资源主要是两方面:
1.内存资源(两个不同进程之间,内存不能共享)
2.打开的文件
也有一些不是共享的资源:
1.上下文 / 状态 / 记账信息 / 优先级(每个进程要独立的参与 CPU 的调度)
2.内存中有一块特殊的区域:栈 (每个线程要独立一份)
进程是系统分配资源的最小单位,线程是系统调度的最小单位(以PCB为单位进行调度)
实际进行并发编程的时候,多线程方式要比多进程方式更常见,也效率更高
本质上和管理进程一样,先用TCB描述,再使用 双向链表
来组织
TCB,(中文的全称:任务控制部件,英文的全拼;task control blocks),主要有任务的优先级,任务的栈空间的地址,以及任务栈的空间的大小,以及用来记录现在执行的任务的pc(程序计数器,当程序执行到的地方)的值。
内核只认 PCB
Linux中,内核里没有线程,只有pcb
即:一个线程和一个 PCB 对应,而一个进程可能和多个 PCB 对应
线程和代码有啥关联关系?
可以认为,一个线程就是代码中的一个执行流~~
执行流:按照一定的顺序来执行一组指令
思考:线程数量是越多越好吗?
不是,因为线程的调度是有开销的,随着线程数量的增多,线程调度的开销也就越大
线程数量太多,非但不会提高效率,反而会降低效率
那么:一个进程中,最多能有多少个线程呢?
假设这个主机有 8核 CPU (两种极端情况:)
现实中的情况是要介于两者之间,实践中一般需要通过 测试 的方式来找到合适的线程数,让这个程序效率够高,同时系统的压力也不会过大
Java 中如何使用多线程?
标准库中提供了一个类:Thread 类
public class ThreadDemo1 {
static class MyThread extends Thread{
@Override
public void run() {
System.out.println("Hello World, 我是一个线程");
}
}
public static void main(String[] args) {
// 创建线程需要使用 Thread 类,来创建一个 Thread 的实例
// 另一方面还需要给这个线程指定 要执行哪些指令/代码
// 指定指令的方式有很多种,此处先用一种简单的,直接继承Thread类,
// 重写 Thread 类中的 run 方法
// 当 Thread 对象被创建出来的时候,内核中并没有随之产生一个线程(PCB)
Thread t = new MyThread();
// 执行这个 start 方法,才是真的创建出一个线程
// 此时内核中才随之出现了一个 PCB,这个 PCB 就会对应让 CPU 来执行该线程的代码(上面的run方法中的逻辑)
t.start();
}
}
输出结果:
代码分析:
为了进一步观察当前确实是俩线程,可以借助第三方工具
JDK 中内置了一个 jconsole
但此时并不能看到线程信息,因为当前进程已经结束了
必须要想办法让进程不要那么快结束,才能看到线程信息
直接安排一个死循环~
public class ThreadDemo1 {
static class MyThread extends Thread{
@Override
public void run() {
// super.run();
System.out.println("Hello World, 我是一个线程");
while (true){
//死循环
}
}
}
public static void main(String[] args) {
Thread t = new MyThread();
t.start();
while (true){
// 死循环
}
}
}
针对一个整数进行大量循环++
串行:
private static long count = 100_0000_0000L;
public static void main(String[] args) {
serial(); //串行
// concurrency(); // 并发
}
private static void serial() {
// 获取当前时间戳
long begin = System.currentTimeMillis();
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.currentTimeMillis();
System.out.println("time: " + (end - begin) + "ms");
}
System.currentTimeMillis( )
— 获取到毫秒级的时间戳
时间戳:
以 1970 年 1月1日 0时0分0秒为基准时刻,计算当前时刻和基准时刻之间的秒数 / 毫秒数 / 微秒数 之差
并发:
private static long count = 100_0000_0000L;
public static void main(String[] args) {
// serial(); //串行
concurrency(); // 并发
}
private static void concurrency() {
long begin = System.currentTimeMillis();
// 匿名内部类
Thread t1 = new Thread(){
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a++;
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
int b = 0;
for (long i = 0; i < count; i++) {
b++;
}
}
};
// 启动线程
t1.start();
t2.start();
try {
// 线程等待,让main 线程等待 t1和t2 执行结束,然后再继续往下执行
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1,t2,和 main 线程之间都是并发执行的
// 调用了 t1.start 和 t2.start之后,两个新线程正在忙着进行计算
// 此时 main线程仍然会继续执行,下面的 end 也就会被随之计算了
// 正确做法: 应该是 t1 t2计算完毕后,再来计算 end 的时间戳
long end = System.currentTimeMillis();
System.out.println("time: " + (end - begin) + "ms");
}
两个线程并发执行的时候,时间大概是 2.7s 左右,时间缩短了很多~
代码示例:
public class ThreadDemo3 {
// Runnable 本质上就是描述了 一段要执行的任务代码是啥
static class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("我是一个新线程~");
}
}
public static void main(String[] args) {
// 1.显式继承 Thread
// 2.通过匿名内部类的方式,继承Thread来创建线程
// Thread t = new Thread(){
// @Override
// public void run() {
//
// }
// };
// t.start();
// 3.显式创建一个类,实现 Runnable 接口
// 然后把 Runnable实例 关联到一个 Thread 实例上
// Thread t = new Thread(new MyRunnable());
// t.start();
// 4.通过匿名内部类的方式,实现Runnable
// Runnable runnable = new Runnable() {
// @Override
// public void run() {
// System.out.println("我是一个新线程~~");
// }
// };
// Thread t2 = new Thread(runnable);
// t2.start();
// 5.使用 lambda 表达式,来指定线程执行的内容
Thread t = new Thread(()->{
System.out.println("我是一个新线程~~~");
});
t.start();
}
}
无论是哪种方式,没有本质上的区别 (站在操作系统的角度),核心都是依靠Thread类,只不过指定线程执行的任务的方式有所差异
细节上有点差别(站在代码耦合性角度
):
通过 Runnable / lambda 的方式来创建线程 和 继承 Thread 类相比,代码耦合性要更小一些,在写 Runnable / lambda 的时候 run 中没有涉及到任何 Thread 相关的内容,这就意味着,很容易把这个逻辑从多线程中剥离出来,去搭配其他的并发编程的方式来执行,当然也可以很容易的改成不并发的方式执行
方法 | 说明 |
---|---|
Thread( ) | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名 |
Thread(Runnable target,String name) | 使用 Runnable 对象创建线程对象,并命名 |
// Thread(String name)
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread("这是一个线程的名字,可以起的很长~"){
@Override
public void run() {
while (true){
}
}
};
t.start();
}
}
代码执行后可以用jconsole查看
属性 | 获取方法 |
---|---|
ID | getId( ) |
名称 | getName( ) |
状态 getState( ) | JVM 中的线程状态 |
优先级 | getPriority( ) |
是否后台线程 | isDaemon( ) |
是否存活 | isAlive( ) |
是否被中断 | isInterrupted( ) |
代码示例:
public class Threadtest {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread("布莱恩特的线程"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
//Thread.currentThread() 获取到当前线程的实例,当前代码中,相当于 this.
System.out.println(Thread.currentThread().getName());
// 效果和上行代码一样
// System.out.println(this.getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// run 方法的执行过程,就代表着系统内线程的生命周期
// run 方法执行中,内核的线程就存在
// run 方法执行完毕,内核中的线程就随之销毁
System.out.println("线程要退出了");
}
};
// t.start();
// 只要线程创建完毕,下面这些属性就不变了,除非显式修改
System.out.println(t.getName());
System.out.println(t.getPriority());
System.out.println(t.isDaemon());
System.out.println(t.getId());
// 这些属性,会随着线程的运行过程而发生改变
System.out.println(t.isAlive());
System.out.println(t.isInterrupted());
System.out.println(t.getState());
t.start();
while (t.isAlive()){
System.out.println("布莱恩特的线程正在运行.....");
System.out.println(t.getState());
System.out.println(t.isInterrupted());
Thread.sleep(300);
}
}
}
补充:
Thread.currentThread( ),即: 获取到当前线程的实例
在当前代码中,相当于 this.
但,不是所有情况都可以使用this
注意:
若使用继承 Thread 的方式来创建线程,这个操作就和 this 是一样的
若使用 Runnable 的方式或者 lambda 的方式,此时就不能使用 this
线程对象被创建出来并不意味着线程就开始运行了
调用 start 方法,才真的在操作系统的底层创建出一个线程
创建实例,和重写 run 方法,是告诉线程要做什么,而调用 start 方法,才是真正开始执行
中断,就是让一个线程结束 — 结束,可能有两种情况:
①已经把任务执行完了;即:让线程 run 执行完(比较温和)
②任务执行了一半,被强制结束,即:调用线程的 interrupt 方法(比较激烈)
常见的线程中断有以下两种方式:
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
// 创建一个线程
Thread t = new Thread(){
@Override
public void run(){
while (!isQuit){
System.out.println("交易继续...");
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("交易被终止!");
}
};
t.start();
Thread.sleep(5000);
System.out.println("发现内鬼,终止交易!");
isQuit = true;
}
上述方式的结束方式比较温和
当标记位被设置之后,等到当前这次循环执行完了之后,再结束线程
例如: 当线程执行到 sleep 的时候,已经 sleep 100ms 了,此时 isQuit 被设置为 true,当前线程不会立刻退出,而是会继续 sleep,把剩下的 400ms sleep 完,才会结束线程
public static void main(String[] args) throws InterruptedException {
// 创建一个线程
Thread t = new Thread(){
@Override
public void run(){
// 此处直接使用线程内部的标记为来判定
// Thread.currentThread() 这个静态方法,获取到当前线程示例
// 哪个线程调用的这个方法,就能获取到对应的实例
while (!Thread.currentThread().isInterrupted()){
System.out.println("交易继续...");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("交易被终止!");
}
};
t.start();
Thread.sleep(1000);
System.out.println("发现内鬼,终止交易!");
t.interrupt();
}
interrupt 本质上是给该线程触发一个异常 InterruptedException,此时,线程内部就会收到这个异常,具体针对这个异常如何处理,这是 catch 内部的事情
例如,上边代码中,catch 中,只是打印了调用栈,并没有真正的结束循环,故应该再加一个 break 结束循环
如果 catch 中没有 break,相当于忽略了异常
如果有 break,则触发异常就会导致循环结束,从而线程也结束
Thread 收到通知的方式有两种:
Thread.interrupted( )
判断当前线程的中断标志被设置,清除中断标志(此方法为借助类里的静态方法判断)Thread.currentThread( ).isInterrupted( )
判断指定线程的中断标志被设置,不清除中断标志(此方法为借助实例,再拿实例里的方法进行判断),这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到
Thread.interrupted( )方式:
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
};
t.start();
t.interrupt();
}
Thread.currentThread( ).isInterrupted( )方式:
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
};
t.start();
t.interrupt();
}
线程和线程之间是并发执行的关系,多个线程之间,谁先执行谁后执行,谁执行到哪里让出 CPU,作为程序员是完全无法感知的,是全权由系统内核负责
例如: 创建一个新线程的时候,此时接下来是主线程继续执行,还是新线程继续执行,这是不好保证的 (这也是 "抢占式执行"的重要特点 )
通过代码验证:
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
@Override
public void run() {
while (true){
System.out.println("我是新线程!");
try {
Thread.sleep(100);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while (true){
System.out.println("我是主线程!");
Thread.sleep(100);
}
}
虽然我们没办法控制哪个线程先走,哪个线程后走,但是我们可以控制,让哪个线程先结束,哪个线程后结束 — 借助线程等待
join 方法:执行 join
方法的线程就会阻塞,一直阻塞到对应线程执行结束之后,才会继续执行
存在的意义: 为了控制线程结束的先后顺序
多线程的一个场景:
例如要进行一个复杂运算,主线程把任务分成几份,每个线程计算自己的一份任务
当所有任务都被分别计算完毕后,主线程再来进行汇总(就必须保证主线程是最后执行完的线程)
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是线程1");
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
Thread t2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我是线程2");
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
t1.join();
t2.start();
t2.join();
}
由于 t1 的 join 放在了 t2 的 strat 之前,意味着此时还没有执行线程2,t1 这里就已经阻塞等待了,一直到 t1 结束,线程2才会继续往下,开始执行
public static Thread currentThread( )
— 返回当前线程对象的引用
代码示例:
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
关于线程休眠:
休眠就是让当前线程进入阻塞队列
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis毫秒 |
public static void sleep(long millis, int nanos) throws InterruptedException | 可以更高精度的休眠 |
线程状态和进程状态是类似的,进程状态是辅助进程进行调度,线程状态是辅助线程进行调度
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()){
System.out.println(state);
}
}
状态说明:
除了 NEW 和 TERMINATED 状态外,其他4个状态的 isAlive 结果都为 true
即,isAlive:判断内核中的线程是否存在
NEW:
Thread 对象有了,内核中的线程(PCB)还没有 即:任务布置了,还没有开始执行RUNNABLE:
就绪状态,表示当前线程正在CPU上执行,或者已经准备好随时上CPU,有一个专门的就绪队列来维护阻塞状态
,当前线程暂时停了下来,不会继续到CPU上执行,表示正在排队 等到时机成熟,才有机会执行BLOCKED:
等待锁导致,这个等待在其他锁释放线程之后被唤醒WAITING:
wait 方法导致,(死等)除非其他线程唤醒了该线程TIMED_WAITING:
sleep 方法导致,结束时间到了就唤醒了TERMINATED:
内核中的线程已经结束(PCB已经被销毁),但是代码中的 Thread 对象还在(这个对象得等GC来回收)
注意:当前这几个状态都是Java的Thread类的状态,和操作系统内部PCB里面的状态的值并不完全一致
yield( ):
主动放权,表示,让当前线程放弃 CPU 的执行权限,重新在就绪队列中排队。这个操作相当于:sleep(0)
使用 isAlive 方法判定线程的存活状态
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run(){
for (int i = 0; i < 1000_0000; i++) {
}
}
};
System.out.println("线程启动前: " + t.getState());
t.start();
while (t.isAlive()){
System.out.println("线程正在运行中: " + t.getState());
}
System.out.println("线程结束后: " + t.getState());
}
线程安全这是个老生常谈的问题了,也是多线程中最核心的话题,同时是最难的话题,还是工作中最相关的话题
多线程并不好驾驭~
正因为如此,有的编程语言中,直接就把线程给干掉了(进行了诸多限制)
Python中的线程,就是“伪线程”,很多时候根本无法实现并发
JS中压根没有线程,只能通过“定时器”+回调 这样的机制凑合实现类似于“并发”的效果
Go中摒弃了线程,但是引入更高端的“协程”,借助“协程”来并发编程(比线程更高效更简单)
public class 线程安全 {
static class Counter{
public int count = 0;
public void increase(){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t2.start();
t1.join();
t2.join();
// 两个线程各自自增5000次,最终预期结果,应该是10w
System.out.println(counter.count);
}
}
观察结果如下按理说应该是10w,但是为什么会出现这样的情况呢?
发生bug的原因
大概率和并发执行相关~~
由于多线程的并发执行,导致代码中出现了逻辑错误,这样的情况就称为“线程不安全”
线程是抢占式执行的 (线程不安全的万恶之源)
原子性:例如自增操作不是原子的
这点也和线程的抢占式调度密切相关. 如果线程不是 “抢占” 的, 就算没有原子性, 也问题不大.
Java 内存模型 (JMM): Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 “工作内存” (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
代码顺序性
如果是在单线程情况下,JVM、CPU指令集会对其进行优化,比如,按 1->3->2的方式执行,也是没问题,可以少跑一次前台。这种叫做 指令重排序
编译器对于指令重排序的前提是 “保持逻辑不发生变化”. 这一点在单线程环境下比较容易判断, 但
是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代
码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论
这个有办法,可以给自增操作加上锁
) 适用范围最广synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
synchronized的功能本质上就是把“并发”变成“串行”,适当的牺牲一下速度,换来的是结果更加准确!
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态
(类似于厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队
理解阻塞等待
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝
试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的
线程, 再来获取到这个锁.
注意
:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这
也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B
和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能
获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
synchronized 的工作过程:
从主内存拷贝变量的最新副本到工作的内存
将更改后的共享变量的值刷新到主内存
synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 "把自己锁死"
一个线程没有释放锁, 然后又尝试再次加锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定, 第二次加锁的时候, 就会阻塞等待. 直到第一次的锁被释放, 才能获取到第
二个锁. 但是释放第一个锁也是由该线程来完成, 结果这个线程已经躺平了, 啥都不想干了, 也就无法进行解锁操作. 这时候就会 死锁.
这样的锁称为 不可重入锁
Java 中的 synchronized 是 可重入锁, 因此没有上面的问题
代码示例
在下面的代码中
这个代码是完全没问题的. 因为 synchronized 是可重入锁.
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
在可重入锁的内部, 包含了 “线程持有者
” 和 “计数器
” 两个信息.
才能被别的线程获取到
)synchronized 本质上要修改指定对象的 “对象头”. 从使用角度来看, synchronized 也势必要搭配一个 具体的对象
来使用.
修饰普通方法
: 锁的 SynchronizedDemo 对象public class SynchronizedDemo {
public synchronized void methond() {
}
}
修饰静态方法
: 锁的 SynchronizedDemo 类的对象public class SynchronizedDemo {
public synchronized static void method() {
}
}
当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
我们重点要理解,synchronized 锁的是什么. 两个线程竞争同一把锁, 才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁, 不会产生竞争.
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.
但是还有一些是线程安全的. 使用了一些锁机制来控制.
还有的虽然没有加锁, 但是不涉及 “修改”, 仍然是线程安全的
代码在写入 volatile 修饰的变量的时候
代码在读取 volatile 修饰的变量的时候
通常来说,如果某个变量在一个线程中读,一个线程中写,这个时候大概率需要使用volatile
volatile这里涉及到一个重要知识点,JVM(java memory model)内存模型
volatile 和 synchronized 有着本质的区别
.
synchronized 能够保证原子性, volatile 保证的是内存可见性
synchronized 既能保证原子性, 也能保证内存可见性.
都是Object
的方法,用来 协同多个线程之间的执行顺序
抢占式其实就是让两个线程执行的顺序关系充满了不确定性,让多个线程直接更好的相互配合
public class ThreadDemo19 {
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
System.out.println("等待前");
object.wait();
System.out.println("等待后");
}
}
预计执行结果:
此代码中,由于没有进行任何的通知机制;所以,预期效果,是一直去等待
实际执行结果:
修改之后的代码:
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object){
System.out.println("等待前");
object.wait();
System.out.println("等待后");
}
}
此时运行程序,就会陷入阻塞,会持续多久,不好说
如何避免 竞态条件问题??
事实上,操作1 和 操作2 在wait 上是原子的
也就是说,只要调用 wait,1 和 2 是一气呵成的,不会先后执行~
notify 方法是唤醒等待的线程
notify操作是一次唤醒一个线程
,如果有多个线程等待,则有线程调度器 随机挑选出一个呈 wait 状态的线程
. (并没有 “先来后到”)public class ThreadDemo20 {
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(){
@Override
public void run(){
synchronized (locker){
while (true){
try {
System.out.println("wait 开始");
locker.wait(); // 要和 synchronized 对应的对象对应
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
};
t1.start();
Thread t2 = new Thread(){
@Override
public void run(){
Scanner scan = new Scanner(System.in);
System.out.println("输入任意一个整数, 继续执行notify() ");
int num = scan.nextInt();
synchronized (locker){
System.out.println("notify 开始");
locker.notify(); // notify 的对象和 wait 的对象要对应,才有效果
System.out.println("notify 结束");
}
}
};
t2.start();
}
}
notify 方法只是唤醒某一个等待线程,使用 notifyAll 方法可以一次唤醒所有的等待线程,这些线程再去竞争同一把锁
单例模式是校招中最常考的设计模式之一.
啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有
一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照
这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 例: MySQL JDBC中,第一步就是创建一个 DataSourse 对象,DataSourse 对象,在一个程序中只有一个实例,不应该实例化多份DataSourse 对象可以用单例模式来解决这种场景,保证指定的类只有一个实例 (若尝试创建多个实例,直接编译就会报错).
单例模式具体的实现方式, 分成 “饿汉
” 和 “懒汉
” 两种.
类加载的同时,创建实例
(只要类被加载,就会立刻实例化 Singleton 实例)
public class ThreadDemo22 {
/*
* 饿汉模式 单例实现
* "饿" —— 只要类被加载,实例就会立刻被创建 (实例创建的时机比较早)
* */
static class Singleton{
// 把构造方法变成私有的,此时在该类的外部就无法 new 这个类的实例了
private Singleton(){
}
// 再来创建一个 static 的成员,表示 Singleton 类唯一的实例
// static 成员 和类相关,和实例是无关的
// 类在内存中只有一份,static 成员也只有一份
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
public static void main(String[] args) {
// 此处 new 可以,是因为 Singleton 是 ThreadDemo22 的内部类,
// ThreadDemo 是可以访问 内部类的 private 成员的
Singleton s = new Singleton();
// 此处的 getInstance 就是获取该类实例的唯一方式,不应该使用其他方式来创建实例
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
当类被加载的时候,不会立刻实例化
等到第一次使用这个实例的时候,再实例化
public class ThreadDemo23 {
/*
* 懒汉模式
* */
static class Singleton{
private Singleton() {
}
// 类加载的时候,没有立刻实例化
// 第一次调用 getInstance 时,才真正的实例化
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
}
类加载的时候,没有立刻实例化;第一次调用 getInstance 时,才真正的实例化
若代码,一直没有调用 getInstance,此时实例化的过程也就被省略掉了 —— 延时加载
一般认为,“懒汉模式” 比 “饿汉模式” 的效率更高~
原因: 懒汉模式有很大的可能是 “实例是用不到”,此时就节省了实例化的开销
(线程安全:假设多个线程并发的调用 getInstance 方法,是否会导致逻辑错误)
啥样的情况会导致线程不安全???
在线程安全问题,我们提到有以下原因:
饿汉模式—线程安全
懒汉模式—线程不安全
分析:
饿汉模式:
实例化时机是在类加载的时候,而类加载只有一次机会,不可能并发执行
当多线程并发的调用 getInstance 时,
由于 getInstance 里只做了一件事
:
读取 instance 实例的地址
相当于多个线程在同时读取同一个变量;因此,饿汉模式是线程安全的
懒汉模式:
多线程同时调用 getInstance 时,getInstance 中做了四件事
:
①读取 instance 的内容;
②判断是否为null;
③若 instance 为 null,就 new 实例;
④返回实例的地址当 new 实例的时候,就会修改 instance 的值
如果实例已经创建完毕,后续再调用getInstance,此时不涉及修改操作,线程仍然是安全的
但是如果实例尚未创建,此时就可能会涉及修改~如果确实存在多个线程同时修改,就会涉及到线程安全问题!!!
懒汉模式,后续调用 getInstance 都不会触发线程安全问题,只有在第一次实例化的时候,多线程并发调用 getInstance 时,会有线程不安全问题的风险
*方法1— 加锁 synchronized
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
instance = new Singleton();
}
}
return instance;
}
private static Singleton instance = null;
public static Singleton getInstance(){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
return instance;
}
上述改法,虽然解决了线程不安全的问题,但仍然会问题 — 效率问题(当实例已经new好了之后,后续每次调用还会加锁,后续本来就没有安全问题,不需要加锁,再尝试加锁就多此一举了,加锁本身是一件开销比较大的操作,多余的加锁会影响代码性能)
private static Singleton instance = null;
synchronized public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
画图和改法2 差不多,只不过 return 操作是在释放锁内部来完成的
由于 return 只是在读,所以这个操作放到锁里边或者锁外边不影响结果
虽然改法2 和 改法3 都可行,但是改法2 的锁粒度更小,改法3 的锁粒度更大
锁的粒度: 锁中包含的代码越多,就认为锁粒度越大
一般,我们希望锁的粒度小一点更好,因为锁的粒度越大,说明这段代码的并发能力就越受限
方法2 — 双重 if
由于加锁是为了避免第一次创建实例时线程不安全,后面再进行加锁解锁操作都只会降低性能,所以外层再添加 if 判断,当发现其为空时才加锁,否则直接返回已经创建好的实例对象,减少了加锁解锁的次数,从而提高性能
private static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
第一批调用这个方法的线程
后续批次调用此方法的线程
为了改进上述可能出现的编译器优化的问题,再添加 volatile==
方法3 — volatile
private volatile static Singleton instance = null;
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
懒汉模式的最终优化结果:
static class Singleton {
private Singleton() {
}
//创建static成员变量,标识Singleton类的唯一实例,为避免内存可见性问题,添加volatile
private volatile static Singleton instance = null;
public static Singleton getInstance() {
// 加锁是为了避免第一个创建实例时线程不安全,后面在进行加锁解锁操作都只会降低性能
if (instance == null) {
//如果为空,说明实例还未存在(即第一次使用),则创建实例
//加锁,确保判断为空和 new对象两个操作 成为原子操作
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关键点总结
1.加锁 — 保证线程安全
2.双重 if — 保证效率
3.volatile — 避免内存可见性引发的问题
以上三点缺一不可
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
阻塞队列的一个典型应用场景就是 “生产者消费者模型
”. 这是一种非常典型的开发模型.
以包饺子为例:
(假设面和馅是备好的,且 擀面杖只有一个~)
方法1: A,B,C 三个人包饺子;这三个人分别进行 擀皮 + 包饺子 过程
该方法的锁竞争(擀面杖只有一个)太激烈
方法2: 一个人负责擀皮,另外两个负责包饺子,例:A 负责擀皮,B 和 C 负责包饺子
这就是"生产者—消费者 模型"
A: 生产者 — (生产饺子皮)
B,C:消费者 — (消费饺子皮)
此处应还有一个"交易场所":放饺子皮的东西,比如:大盘子~
阻塞队列就是生产者—消费者生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,
服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放
到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.
这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺
子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人
也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超
市买的).
阻塞队列特点:
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
实现阻塞队列前,我们先思考普通队列是如何实现的??
有两种方式:
实现功能
class BlockingQueue{
private int[] array = new int[10];
// [head,tail)
// 两者重合时,队列可能为空,也可能为满
private volatile int head = 0;
private volatile int tail = 0;
private volatile int size = 0; // 有效元素个数
/*
* 阻塞队列 入队列
* 为了和普通队列入队列区分,使用 put
* */
public void put(int value) throws InterruptedException {
synchronized (this){
// 若队列满了, 阻塞等待, 等下面的出队列操作调用 notify 方法后才可继续执行
if(size == array.length){
wait();
}
array[tail] = value;
tail++;
if(tail == array.length){
tail = 0;
}
size++;
// 唤醒 出队列操作
notify();
}
}
/*
* 阻塞队列 出队列
* 为了和普通队列出队列区分,使用 take
* */
public int take() throws InterruptedException {
int ret = -1;
synchronized (this){
// 若队列为空, 阻塞等待, 等到有元素入队列再开始
if(size == 0){
wait();
}
ret = array[head];
head++;
if(head == array.length){
head = 0;
}
size--;
// 唤醒 入队列操作
notify();
}
return ret;
}
}
代码分析:
上述两个 wait( ) 是一定不可能被同时调用的!!
入队列操作的 wait( )
当队列已满时,即:size == array.length;若有线程调用 put 方法,就让其执行 wait 方法,使该线程阻塞等待,直到有其他线程调用 take 方法,取出元素后,调用 notify 方法将刚才调用 put 方法产生阻塞的线程唤醒,接着继续执行 put 后续的操作
出队列操作的 wait( )
当队列为空时,即:size == 0;若有线程调用 take 方法,就让其执行 wait 方法,使该线程阻塞,直到有其他线程调用 put 方法,插入元素后,调用 notify 方法将刚才调用 take 方法产生阻塞的线程唤醒,接着继续执行 take 后续的操作
测试代码:
创建两个线程,分别模拟 生产者 和 消费者
以第二种为例
public static void main(String[] args) {
BlockingQueue blockingQueue = new BlockingQueue();
// 创建两个线程,分别模拟 生产者 和 消费者
Thread producer = new Thread(){
@Override
public void run(){
for (int i = 0; i < 10000; i++) {
try {
blockingQueue.put(i);
System.out.println("生产元素: " + i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
producer.start();
Thread consumer = new Thread(){
@Override
public void run(){
while (true){
try {
int ret = blockingQueue.take();
System.out.println("消费元素: " + ret);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
consumer.start();
}
假设把上述代码中的 notify( ) 改成 notifyAll( ),此时会发生什么??
假设现在有三个线程,其中一个线程生产,两个线程消费,且消费速度快于生产速度
所以,两个消费者线程都触发了 wait 操作,也就是都发生了阻塞;当我们调用 notifyAll( ) ,会将上述两个线程都唤醒,然后这两个线程都去尝试重新竞争锁
假设:
消费者1,先获取到锁,于是执行出队列操作(执行完毕释放锁)
消费者2,后获取到锁,于是也会执行后续的出队列操作,但是刚才生产者生产的一个元素,已经
被消费者1线程 给取走了,即,当前实际是一个空的队列,若强行执行出队列操作,就会出现逻辑上的错误!!
改正方法:
将 wait( ) 方法包裹的 if 改为 while
此时两个消费者线程尝试竞争锁
消费者1,先获取到锁,wait( ) 就返回了,再次执行 while 中的条件(由于当前生产者线程生产了一个元素,size 不为0 ),循环退出,之后,消费者1 就可以执行后续出队列操作,执行完毕后,释放锁
消费者2,后获取到锁,wait( ) 返回,再次执行 while 中的条件(由于刚才的消费者1 已经把生产的元素取走了,size 又是 0),循环继续执行,又一次调用 wait( ),只能继续等…
定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码.
定时器是一种实际开发中非常常用的组件.
比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
类似于这样的场景就需要用到定时器.
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello");
}
}, 3000);
定时器的构成:
一段逻辑
" (一个要执行的任务 task ),同时也要记录该任务在啥时候来执行使用一个 阻塞优先队列
来组织若干个任务,让队首元素是最早执行的任务,只检测队首元素是否到了时间即可代码实现:
优先队列中的元素必须是可比较的
:
比较规则的指定主要有两种方式:
// 1.用一个类来描述任务
static class Task implements Comparable<Task>{
private Runnable command; // 当前任务
private long time; // 开始执行的时间
/*
* command: 当前任务
* after: 多少ms后执行,表示一个相对时间
* */
public Task(Runnable command, long after) {
this.command = command;
this.time = System.currentTimeMillis() + after;
}
// 指定任务的具体逻辑
public void run(){
command.run();
}
@Override
public int compareTo(Task o) {
//谁的时间小 谁先执行
return (int) (this.time - o.time);
}
}
Timer 实例中, 通过 PriorityBlockingQueue 来组织若干个 Task 对象.
通过 schedule 来往队列中插入一个个 Task 对象
static class Timer{
// 2.用一个阻塞优先队列来组织多干个任务,让队首元素是执行时间最早的元素
// 标准库中的阻塞优先队列
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
public Timer(){
Worker worker = new Worker(queue);
worker.start();
}
/*
* 4.提供一个方法,让调用者添加任务
* */
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
queue.put(task);
}
}
worker 线程, 一直不停的扫描队首元素, 看看是否能执行这个任务
/*
* 3.用一个线程,循环扫描检测当前阻塞队列中的队首元素,若时间到,就执行指定任务
* */
static class Worker extends Thread{
private PriorityBlockingQueue<Task> queue = null;
public Worker(PriorityBlockingQueue<Task> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true){
try {
// 1.取队首元素,检查是否已到时间
Task task = queue.take();
// 2.检查当前任务是否已到时间
long curTime = System.currentTimeMillis();
if(task.time > curTime){
//时间还没到, 就把任务再 送回队列中
queue.put(task);
}
else {
// 时间到了, 直接执行
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
// 若线程出现问题,停止循环
break;
}
}
}
}
测试代码:
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new Runnable() {
@Override
public void run() {
System.out.println("呵呵~");
timer.schedule(this,2000);
}
},2000);
}
忙等
上述代码明显存在一个严重问题:扫描线程在 “忙等”
扫描线程在循环扫描判断队首元素是否到了其发生时间,若时间一直未到,就会一直循环扫描,造成了无意义的CPU浪费
例: 早上8:30要上课,定了个8:00的闹钟,睁开眼看了下时间,发现是7:00,还有一个小时闹铃才响,再看了一眼时间,7:01,时间还没到,难道接下来一直看表嘛???每分钟看一次??每秒看一次??这样无疑是浪费精力的,且是没有意义的。这种情况就叫忙等
为了避免忙等,我们可以借助 wait( ) 来解决
在wait 和 nitify 里,我们提到了 wait( ) 的两种用法
当扫描线程发现当前队首元素还未到指定时间时,调用 wait( )方法,使线程阻塞,减少不必要的循环扫描判断,避免了频繁占用CPU;等待时间:任务发生时间 -当前时间
BUT 若在等待的过程中,插入了其他任务时间比当前任务早执行的任务,怎么办呢?
解决方法:
1.扫描线程内部,加上wait
2.添加任务方法内部,加上notify
// 2.检查当前任务是否已到时间
long curTime = System.currentTimeMillis();
if(task.time > curTime){
//时间还没到, 就把任务再 送回队列中
queue.put(task);
synchronized (locker){
locker.wait(task.time - curTime);
}
}
/*
1. 4.提供一个方法,让调用者添加任务
2. */
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
queue.put(task);
synchronized (locker){
locker.notify();
}
}
两种阻塞情况:
当队列为空时,在 take 处阻塞
当阻塞队列为空时,出现阻塞,一旦调用 schedule方法,添加了新任务,其后的 notify 方法将唤醒这个线程
若队列非空,时机还没到,就在wait 处阻塞
①插入的任务早于当前队首任务时间,这时队首元素将变为新的任务,再次执行之后的判断即可
②插入的任务等于或晚于当前队首任务时间,扫描线程继续阻塞
附最终全部代码:
/*
* 定时器
* */
public class ThreadDemo26 {
// 1.用一个类来描述任务
static class Task implements Comparable<Task>{
private Runnable command; // 当前任务
private long time; // 开始执行的时间
/*
* command: 当前任务
* after: 多少ms后执行,表示一个相对时间
* */
public Task(Runnable command, long after) {
this.command = command;
this.time = System.currentTimeMillis() + after;
}
// 指定任务的具体逻辑
public void run(){
command.run();
}
@Override
public int compareTo(Task o) {
//谁的时间小 谁先执行
return (int) (this.time - o.time);
}
}
static class Timer{
// 为了避免忙等,需要使用wait 方法,使用一个单独的对象,来辅助进行wait
private Object locker = new Object();
// 2.用一个阻塞优先队列来组织多干个任务,让队首元素是执行时间最早的元素
// 标准库中的阻塞优先队列
private PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>();
public Timer(){
Worker worker = new Worker(queue,locker);
worker.start();
}
/*
* 4.提供一个方法,让调用者添加任务
* */
public void schedule(Runnable command,long after){
Task task = new Task(command,after);
queue.put(task);
synchronized (locker){
locker.notify();
}
}
}
/*
* 3.用一个线程,循环扫描检测当前阻塞队列中的队首元素,若时间到,就执行指定任务
* */
static class Worker extends Thread{
private PriorityBlockingQueue<Task> queue = null;
private Object locker = null;
public Worker(PriorityBlockingQueue<Task> queue,Object locker) {
this.queue = queue;
this.locker = locker;
}
@Override
public void run() {
while (true){
try {
// 1.取队首元素,检查是否已到时间
Task task = queue.take();
// 2.检查当前任务是否已到时间
long curTime = System.currentTimeMillis();
if(task.time > curTime){
//时间还没到, 就把任务再 送回队列中
queue.put(task);
synchronized (locker){
locker.wait(task.time - curTime);
}
}
else {
// 时间到了, 直接执行
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
// 若线程出现问题,停止循环
break;
}
}
}
}
}
在String内部,为了进行一些优化,引入了一个"字符串常量池"
线程池是一种多线程处理形式,处理过程中可以 将任务添加到队列中
,然后 再创建线程后自动启动这些任务
使用线程池最大的原因是可以根据系统的需求和硬件环境灵活的控制线程的数量,可以对所有线程进行统一的管理和控制,从而提高系统运行效率,避免了频繁创建 / 销毁线程的开销
需要管理两个内容:①要执行的任务,②执行任务的线程们
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
Executors 创建线程池的几种方式
(Executors 本质上是 ThreadPoolExecutor 类的封装)
线程池 核心操作
execute
把一个任务加到线程池中
public void execute(Runnable command) throws InterruptedException {
// 使用 延时加载 的方式来创建线程
// 当线程池中的线程数量少于阈值,则创建新线程执行该任务
// 否则添加进队列,等待其他线程结束之前的任务再执行该任务
if(pool.size() < MAX_WORKER_COUNT){
// 创建新线程
Worker worker = new Worker(queue);
// 执行任务
worker.start();
// 将 worker 添加到 线程池中
pool.add(worker);
}
queue.put(command);
}
shutdown
销毁线程池中的所有线程
调用每个线程的interrupt方法,使线程中断;调用 interrupt 后,每个线程并不是立即结束,而是可能等待一段时间,所以需要再使用 join 方法,来等待每个线程都执行结束
第一个循环触发异常,终止线程
第二个是等待每个线程结束
public void shutdown() throws InterruptedException {
// 终止掉所有的线程
for(Worker worker : pool){
worker.interrupt();
}
// interrupt后,每个线程不是立刻结束
// 需等待每个线程执行结束
for (Worker worker : pool){
worker.join();
}
}
完整实现代码:
public class ThreadDemo27 {
// 使用这个类描述 当前的工作线程
static class Worker extends Thread{
// 每个 worker 线程都需要从任务队列中取任务
// 需要能够获取到任务队列中的实例
private BlockingQueue<Runnable> queue = null;
public Worker(BlockingQueue<Runnable> queue) {
this.queue = queue;
}
@Override
public void run(){
// 此处 while被try 包裹: 只要线程收到异常,就会立刻结束 run 方法(结束线程)
try {
while (!Thread.currentThread().isInterrupted()){
Runnable command = queue.take();
command.run();
}
}
catch (InterruptedException e){
// 线程被结束
System.out.println(Thread.currentThread().getName() + " 线程结束");
}
}
}
static class MyThreadPool{
//线程池中最多可同时执行的线程数量
private static final int MAX_WORKER_COUNT = 5; // 一个线程内部应该有多少个线程,应该根据实际情况来定
// 阻塞队列: 用于组织若干个任务
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
// List: 用于组织若干个工作线程
private List<Worker> pool = new ArrayList<>();
/*
* 实现 execute 方法:
*/
public void execute(Runnable command) throws InterruptedException {
// 使用 延时加载 的方式来创建线程
// 当线程池中的线程数量少于阈值,则创建新线程执行该任务,否则添加进队列,等待其他线程结束之前的任务再执行该任务
if(pool.size() < MAX_WORKER_COUNT){
// 创建新线程
Worker worker = new Worker(queue);
// 执行任务
worker.start();
// 将 worker 添加到 线程池中
pool.add(worker);
}
queue.put(command);
}
/*
* 实现 shutdown 方法:
*/
public void shutdown() throws InterruptedException {
// 终止掉所有的线程
for(Worker worker : pool){
worker.interrupt();
}
// interrupt后,每个线程不是立刻结束
// 需等待每个线程执行结束
for (Worker worker : pool){
worker.join();
}
}
}
}
线程池: 本质上是一个生产者—消费者模型
生产者: 调用 execute 的代码就是生产者,生产者成产了任务 (Runnable 对象)
消费者: Woker 线程 就是消费者,消费了队列中的任务
交易场所: BlockingQueue
当最初创建线程池实例的时候,此时线程池中没有线程
- MyThreadPool pool = new MyThreadPool();
继续调用 execute,就会触发创建线程操作
- pool.execute(new Command(i));
这个我们无力改变,这是操作系统内核实现的
)