1. 基本概念
1.1 程序、进程、线程
程序(Program)是计算机执行的一系列指令的有序集合,它由开发人员使用编程语言编写而成。程序通常被保存在磁盘或其他存储介质上,并在需要时加载到计算机的内存中执行。
进程(Process)是操作系统中的一个执行单位。它代表了正在运行的程序的实例。每个进程都有自己独立的内存空间和系统资源,包括打开的文件、分配的内存等。进程之间相互独立,彼此隔离。每个进程都有一个唯一的进程标识符(Process ID),用于在操作系统中进行标识和管理。
线程(Thread)是进程中的一个执行路径。一个进程可以拥有多个线程,它们共享进程的内存空间和系统资源。线程是操作系统调度和执行的基本单位。多线程的程序可以同时执行多个线程,因此可以提高程序的并发性和响应性。
- 程序是静态的代码集合,而进程和线程是程序的动态执行状态。
- 进程是操作系统中的执行实体,具有独立的内存空间和系统资源;而线程是进程的执行路径,共享进程的资源。
- 进程之间相互独立,彼此隔离,但线程是在同一个进程内执行,共享同一份内存空间。
- 进程之间通信需要使用特定的机制,如管道、共享内存等;而线程之间可以直接通过共享内存进行通信。
总结:程序是静态的代码集合(开发人员编写并存储在磁盘或存储介质中),进程是程序的运行实例(运行中程序)线程则是进程的执行路径。多线程可以提高程序的并发性和响应性。进程之间则相互独立,线程共享进程的资源。
1.2 并行和并发
并行: 多核cpu下,多线程同时执行
并发:两个或多个任务按照交替的方式执行 可以提高系统的吞吐量和响应性。
总结:并行是真正的同时执行,并发则是交替执行。并行和并发不是互斥的概念,在某些情况下可以同时使用并行和并发来提高系统性能
1.3 线程使用场景
1.提高程序性能 ,多线程可以利用多核处理器的并行计算能力,提高程序的处理速度和性能。
2.并发处理I/o操作 如网络通信、文件读写等等
3.后台任务:在程序中执行一些耗时间的后台任务时可以使用线程来执行,以保持主线程的相应。
2. 多线程实战
2.1 Thread方式实现多线程
- 定义子类继承Thread类。
- 类中重写Thread类中的run方法。
- 创建Thread子类对象,即创建了线程对象。
- 调用线程对象start方法:启动线程,调用run方法。
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 10000; i++) {
if (i % 2 == 0) {
//Thread.currentThread().getName():得到线程的名字
System.out.println(Thread.currentThread().getName() + "\t" + i);
}
}
}
}
//启动一个线程,在线程中执行1到1万的偶数打印工作
@Test测试
public class Test1 {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.start();
System.out.println(Thread.currentThread().getName() + " main 线程 over");
}
}
产生图形界面,主线程进入IO阻塞
子线程执行,进程不会停止
2.2 Runnable多线程
- 定义子类,实现Runnable接口。
- 类中重写Runnable接口中的run方法。
- 通过Thread类含参构造器创建线程对象。
- 将Runnable接口的子类对象作为实际参数传递给Thread类的构造器中。
- 调用Thread类的start方法:开启线程, 调用Runnable子类接口的run方法。
public class TestRunnable {
// 先实现Runnable接口,在通过Thread类创建对象,
// 将Runnable的对象作为实际参数传给Thread的构造函数中
public static void main(String[] args) {
// 方式1:Thread子类启动
MyThread myThread1 = new MyThread();
myThread1.start();
// 方式2:Runnable接口方式启动
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable,"t1");
thread1.start();
Thread thread2 = new Thread(myRunnable,"t2");
thread2.start();
//方式3 ,匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}).start();
System.out.println("线程结束");
}
}
2.3 sleep
指定线程休眠的时间,让出cpu时间片,其他线程可以抢占cpu时间片
public class MyTask implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 100; i++) {
if (i % 2 == 0) {
//Thread.currentThread().getName():得到线程的名字
System.out.println(Thread.currentThread().getName() + "\t" + i);
try {
Thread.sleep(100);
Thread.yield();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
是无序的
2.4 线程优先级
从1到10
-
- MAX_PRIORITY: 10
- MIN _PRIORITY: 1
- NORM_PRIORITY: 5
MyRunnable myRunnable = new MyRunnable();
Thread thread1 = new Thread(myRunnable,"t1");
thread1.setPriority(Thread.MIN_PRIORITY);
thread1.start();
Thread thread2 = new Thread(myRunnable,"t2");
thread2.setPriority(Thread.MAX_PRIORITY);
thread2.start();
在理论上,线程优先级是有序的,优先级较高的线程应该在优先级较低的线程之前得到执行。然而,在实际情况下,线程优先级的有序性并不能完全保证。
具体来说,线程优先级的有序性受到操作系统和硬件的影响。不同的操作系统可能会有不同的调度算法和策略,以确定哪个线程应该获得CPU资源。硬件因素也会对线程优先级的有序性产生影响。
2.5 守护线程
- 其他线程都执行结束,守护线程自动结束
- 守护启动子线程,也是守护线程
- 守护线程的语法
thread.(setDaemon(true)
设置守护线程
案例
public class ThreadShouHu extends Thread{
@Override
public void run() {
while (true) {
System.out.println("奥里给");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
Thread myThread = new ThreadShouHu();
myThread.setDaemon(true);
myThread.start();
for (int i = 1; i <100 ; i++) {
System.out.println("over");
}
// JOptionPane.showMessageDialog(null,"是否确认向下执行。。。");
// System.out.println("main over");
}
}
创建了一个名为MyThread
的实现了Thread
类的自定义线程。该线程在run
方法中以循环方式打印一条消息,然后暂停1秒。由于该线程被设置为守护线程,当主线程结束时,守护线程也会被自动终止。
需要注意的是,守护线程并不保证会在程序结束时立即终止,因为它们可能还在执行中。如果守护线程执行一些需要时间的操作,可能需要等待其完成才能终止。
2.6 线程合并
public class JOIN {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <100; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});t1.start();
try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+ "main over");
}
}
当主线程(main线程)调用t1.join()方法时,它会等待自定义线程t1执行完毕后再继续执行。
所以,在这种情况下,自定义线程t1会先执行,而主线程会等待t1执行完毕后才会继续执行后续的代码。具体的执行顺序如下:
- 主线程开始执行。
- 主线程启动自定义线程t1并调用t1.join()方法。
- 主线程进入等待状态,等待t1执行完毕。
- 自定义线程t1开始执行。
- t1执行完毕后,主线程继续执行后续的代码。
请注意,执行顺序也受到线程调度器的影响。线程调度器负责决定各个线程之间的执行顺序和时间片分配,因此具体的执行结果可能会有微小的差异。
2.7 线程退出
2.8 中断信号 (优雅退出)
在Java中,中断是一种线程间的通信机制,用于请求某个线程终止正在执行的任务,可以通过中断信号(interrupt)来实现线程的中断。
中断信号是一种在多线程编程中用于通知线程应该中止执行的机制。Java中的中断机制允许一个线程发送中断信号给另一个线程,以请求它停止执行或进行其他适当的处理。
以下是Java中关于中断的机制和行为:
- 中断标志位:每个线程都有一个
boolean
类型的中断标志位,表示线程的中断状态。初始时,中断标志位为false
。 interrupt()
方法:通过调用线程对象的interrupt()
方法,可以将线程的中断标志位设置为true
,即发送中断信号给线程。isInterrupted()
方法:调用isInterrupted()
方法查询自己的中断标志位,以判断是否收到中断信号。线程可以通过如果中断标志位为true
,则表示线程已经收到中断信号。InterruptedException
异常:一些阻塞操作,例如Thread.sleep()
、Object.wait()
等,在收到中断信号时可能会抛出InterruptedException
异常。这个异常可以被捕获,并相应地处理中断请求。Thread.interrupted()
方法:静态方法Thread.interrupted()
用于检查当前线程的中断状态,并清除中断标志位。如果中断标志位为true
,则返回true
;否则,返回false
。这个方法可以用于判断线程是否收到中断信号,并清除中断标志位。
当线程收到中断信号时,可以根据具体的应用场景和业务逻辑来决定如何处理。通常,线程可以立即终止执行、进行清理操作、抛出异常或者采取其他适当的行为。
需要注意的是,中断只是一种请求,线程本身需要检查中断状态,并在适当的时机响应中断请求。并不是所有的线程操作都会受到中断的影响,比如一些CPU密集型操作不会主动检查中断状态,需要线程自行判断是否响应中断。
public class InterruptDemo3 extends Thread{
public static void main(String[] args) {
Thread thread1 = new Thread();
thread1.start();
//得到当前线程状态,初始为false
System.out.println(thread1.isInterrupted());
//发送中断信号,获取当前线程状态,true
thread1.interrupt();
System.out.println(thread1.isInterrupted());
//得到中断信号后转化为false
System.out.println(thread1.interrupted());
}
}
2.8.1 小案例:
public class InterruptDemo2 extends Thread {
@Override
public void run() {
while(!Thread.currentThread().isInterrupted()){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("线程被中断");
break;
}
}
System.out.println("thread1 终止 任务结束");
}
public static void main(String[] args) {
InterruptDemo2 thread1 = new InterruptDemo2();
thread1.start();
while (true){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread1.interrupt();
System.out.println("线程未被中断");
if (thread1.isAlive()){
break;
}
}
}}
3.线程原理
3.1 线程的调度与时间片
由于 CPU 的计算频率非常高,每秒计算数十亿次,于是,可以将 CPU 的时间从毫秒的维度进行分段,每一小段叫做一个 CPU 时间片。不同的操作系统、不同的处理器,线程的 CPU 时间片长度都不同。假定操作系统的线程一个时间片的时间长度为 20 毫秒(比如 Windows XP),在一个 2GHz 的 CPU 上,那么一个时间片可以进行计算的次数是: 20 亿/(1000/20) =4 千万次,也就是说,一个时间片内的计算量是非常巨大的。 目前操作系统中主流的线程调度方式大都是:基于 CPU 时间片方式进行线程调度。线程只有得到 CPU 时间片,才能执行指令,处于执行状态;没有得到时间片的线程,处于就绪状态,等待系统分配下一个 CPU 时间片。由于时间片非常短,在各个线程之间快速地切换,表现出来特征是很多个线程在“同时执行”或者“并发执行”。线程的调度模型,目前主要分为两种调度模型:分时调度模型、抢占式调度模型。
(1)分时调度模型——系统平均分配 CPU 的时间片,所有线程轮流占用 CPU。分时调度模型在时间片调度的分配上,所有线程人人平等。
下图就是一个分时调度的简单例子:三个线程,轮流得到 CPU 时间片;一个线程执行时,另外两个线程处于就绪状态
(2)抢占式调度模型——系统按照线程优先级分配 CPU 时间片。优先级高的线程,优先分配 CPU 时间片;如果所有的就绪线程的优先级相同,那么会随机选择一个;优先级高的线程获取的 CPU 时间片相对多一些。 由于目前大部分操作系统都是使用抢占式调度模型进行线程调度。 Java 的线程管理和调度是委托给了操作系统完成的,与之相对应, Java 的线程调度也是使用抢占式调度模型。
3.2 线程状态
3.2.1 操作系统,线程的状态
- New(新建状态):当通过创建系统调用或库函数创建一个新的线程时,线程处于新建状态。
- Runnable(运行状态):操作系统将线程调度到可运行队列中,线程获得 CPU 时间片后,进入运行状态。
- Waiting(等待状态):线程可能由于某些原因而暂时停止执行,进入等待状态,例如等待某个条件满足、等待其他线程的完成等。
- Timed Waiting(计时等待状态):与等待状态类似,但指定了等待的时间限制,超过时间后线程会自动转到就绪状态。
- Blocked(阻塞状态):线程可能因为某些原因(如等待锁、IO操作等)被阻塞,此时处于阻塞状态。
- Terminated(终止状态):线程执行完任务或发生了未捕获的异常而终止时,进入终止状态。
新建(.start) -> 就绪(获取cpu执行权) -> 运行(sleep (long time)) join(线程合并)
-> 阻塞状态 -> 阻塞状态结束 -> 运行 -> 任务结束 进入死亡
3.2.2 Java的线程状态和生命周期
- New(新建状态):当创建了一个 Thread 对象但尚未调用 start() 方法时,线程处于新建状态。
- Runnable(就绪状态):线程被启动后进入就绪状态,表示该线程已经具备执行的条件,等待 CPU 时间片的分配。
- Running(运行状态):就绪状态的线程获得 CPU 时间片后,进入运行状态,开始执行线程的 run() 方法。
- Waiting(等待状态):线程可能由于某些原因而暂时停止执行,进入等待状态,例如等待某个条件满足、等待其他线程的完成等。
- Timed Waiting(计时等待状态):与等待状态类似,但指定了等待的时间限制,超过时间后线程会自动转到就绪状态。
- Blocked(阻塞状态):线程可能因为某些原因(如等待锁、IO操作等)被阻塞,此时处于阻塞状态。
- Terminated(终止状态):线程执行完 run() 方法或抛出未捕获的异常而终止时,进入终止状态。
public Thread.State getState();
//返回当前线程的执行状态,一个枚举类型值
枚举展示:Thread.State 是内部枚举类,定义了 6 个枚举常量,分别代表 Java 线程的 6 种状态
public static enum State {
NEW, //新建
RUNNABLE, //可执行:包含操作系统的就绪、运行两种状态
BLOCKED, //阻塞 -> 操作系统线程中的阻塞
WAITING, //等待 -> 操作系统线程中的阻塞
TIMED_WAITING, //计时等待 -> 操作系统线程中的阻塞
TERMINATED; //终止
}
3.3 多线程自增i++ 和 线程执行原理
public class Plus {
private int amount = 0;
public void selfPlus() {
amount ++;
}
public int getAmount() {
return amount;
}
}
public class PlusTask implements Runnable {
private Plus plus;
public PlusTask() {
}
public PlusTask(Plus plus) {
this.plus = plus;
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
plus.selfPlus();
}
}
}
public static void main(String[] args) throws InterruptedException {
Plus plus = new Plus();
PlusTask plusTask = new PlusTask(plus);
Thread t1 = new Thread(plusTask);
Thread t2 = new Thread(plusTask);
Thread t3 = new Thread(plusTask);
Thread t4 = new Thread(plusTask);
t1.start();
t2.start();
t3.start();
t4.start();
t1.join();
t2.join();
t3.join();
t4.join();
System.out.println("实际的值 = " + plus.getAmount());
}
结果为一万多,因为会有多个线程同时访问的情况
4. 多窗口买票
案例实现:
后台库存:
public class Ticket1 {
private int count = 100;
/**
* 查票
* @return
*/
public int getCount() {
return count;
}
public int out() {
return this.count--;
}
}
窗口卖票任务:
public class TICKEDTask implements Runnable{
private Ticket1 ticket1;
public TICKEDTask() {
}
public TICKEDTask(Ticket1 ticket1) {
this.ticket1 = ticket1;
}
@Override
public void run() {
while (true){
if (ticket1.getCount() <= 0){
break;
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + ticket1.out());
}
}
}
实现Runnable接口,引用库存类,构造函数传参,重写Runnable的Run方法。循环卖票小于0时结束循环。
打印线程名称,和剩余的票数量(out方法)
运行类
public class TEST {
public static void main(String[] args) {
Ticket1 ticket1 = new Ticket1();
TICKEDTask task = new TICKEDTask(ticket1);
Thread window1 = new Thread(task,"1窗口");
Thread window2 = new Thread(task,"2窗口");
Thread window3 = new Thread(task,"3窗口");
Thread window4 = new Thread(task,"4窗口");
Thread window5 = new Thread(task,"5窗口");
Thread window6 = new Thread(task,"6窗口");
Thread window7 = new Thread(task,"7窗口");
window1.start();
window2.start();
window3.start();
window4.start();
window5.start();
window6.start();
window7.start();
}
}
运行结果: 出现了超卖问题,因为Thread.sleep(1000);
1窗口 3
2窗口 2
3窗口 0
3窗口 1
4窗口 -1
2窗口 2
4窗口 2
4窗口 -2
1窗口 -3
超卖问题原因:
- 超卖问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
- 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
4.2 临界区代码块
多个线程同时访问的代码块称为临界区代码块
共享数据:临界区代码块中,多个线程共享访问的堆里的数据(例:ticket)
通过共享数据给临界区代码块加锁
5. synchronized内置锁
重复一下基本的两个概念:
1.临界区资源(共同数据)
表示一种可以被多个线程使用的公共资源或共享数据,但是每一次只能有一个线程使用它,一旦临界区资源被占用,想使用该资源的其他线程必须等待。
2.临界区代码段(Critical Section)
是每个线程中访问临界资源的那段代码,多个线程必须互斥的对临界区资源进行访问。线程在进入临界区代码段之前,必须在进入区申请资源,申请成功之后进行临界区代码段,执行完之后释放资源。 下图是临界区代码段的进入和退出:
5.1 语法
1.代码块
synchronized (共享对象(plus,ticket)) {//临界区代码块
//对共对象的访问(plus)(ticket)
}
5.1.2 多窗口超卖问题解决
对这个ticket1 进行加锁,死循环检查getCount(票数)等于0时候跳出循环,并且不会发生超卖问题
@Override
public void run() {
synchronized (ticket1){
while (true){
if (ticket1.getCount() <= 0) {
break;
}
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "\t" + ticket1.out());
}
}}
5.1.3 自增1千次案例解决
相当于是调用了1万次selfPlus方法,这个方法里含count++,加锁后实现多线程不会出现次数不达到。
public void run() {
synchronized (zZeng) {
for (int i = 0; i < 10000; i++) {
zZeng.selfPlus(); //amount++方法内容
}
}
5.2 实例方法加锁
fun方法为第一种加锁方式,punlic synchronized void fun(){ }
fun2方法为第二种加锁方式,synchroized(this) + 方法实现
public synchronized void fun(){
// synchronized (this){
System.out.println("开始打蜡");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("打蜡结束");
}
public void fun2(){
synchronized (this){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("开始抛光");
System.out.println("抛光结束");
}
}
汽车打蜡方法1和汽车抛光方法1 两种都是为方法上锁
5.3 死锁
Synchronized嵌套时,比如张三在自己家想去李四家,李四在自己家想去张三家,这时就会发生死锁, A线程锁定一个资源同时想获取B线程的资源,B线程锁定一个资源同时想获取A线程的资源。
5.3.1 案例
public class DieSuo {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1){
System.out.println("获取到了lock1的锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("获取到了lock2的锁");
}
}});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (lock1){
System.out.println("获取到了lock1的锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("获取到了lock2的锁");
}
}});
t1.start();
t2.start();
}
}
创建两个线程thread1
和thread2
,并且定义了两个共享的锁对象lock1
和lock2
。
在thread1
的run()
方法中,它首先获取了lock1
的锁,并输出了一条消息。然后,它在一段时间内睡眠(模拟执行一些操作)。接下来,它尝试获取lock2
的锁。如果lock2
的锁没有被其他线程持有,那么thread1
会成功获取锁,并输出一条消息表明它已获取lock2
的锁。在thread2
的run()
方法中,它与thread1
的逻辑类似。然而,因为线程执行的顺序不确定,有可能出现以下情况:
thread1
获取了lock1
的锁。- 同时,
thread2
获取了lock2
的锁。 thread1
尝试获取lock2
的锁,但此时被thread2
持有。thread2
尝试获取lock1
的锁,但此时被thread1
持有。
此时,thread1
在等待lock2
的释放,而thread2
在等待lock1
的释放。由于两个线程相互等待对方释放锁,它们将进入死锁状态,并无法继续执行。
这个代码示例展示了典型的死锁情况,其中两个线程相互等待对方持有的锁,导致程序无法正常结束。需要注意的是,实际发生死锁的概率取决于线程调度的具体顺序。
5.4 CountDownLatch
CountDownLatch是java中一个同步工具类,可以用于控制线程的执行顺序。CountDownLatch
的作用是允许一个或多个线程等待其他线程完成操作之后再继续执行。它通过一个计数器来实现,该计数器在初始化时设置一个初始值,并且每当某个线程完成了预期的操作后,计数器的值就会减少。当计数器值达到零时,表示所有需要等待的操作已经完成,等待中的线程便可以继续执行。
5.4.1 提供方法
1.await():线程调用await()方法会被阻塞,直到计数器值变为0才会停止
2.countdown():用于减少计数器的值,表示一个操作已经完成,每次调用此方法,计数器值-1.
5.4.2 案例
public class CountDownLatchDemo1 {
//执行自增
private int count = 0;
public void self() {
count++;
}
public int getCount() {
return count;
}
自增类
}
public class MyTaskCDL implements Runnable{
CountDownLatchDemo1 c1 ;
CountDownLatch countDownLatch;
public MyTaskCDL() {
}
public MyTaskCDL(CountDownLatchDemo1 c1, CountDownLatch countDownLatch) {
this.c1 = c1;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
synchronized (c1){
for (int i = 0; i < 100000000; i++) {
c1.self();
}
countDownLatch.countDown(); //-1
}
}
}
调用自增类命名为c1,创建CountDownLatch对象,构造函数传参
重写Run方法循环调用自增类的自增方法1亿次。并且对此任务加了synchronized锁,保证同时只有一个线程调用此方法。每个线程调用完成后countDown方法计数器值—1.
5.4.2.1 测试类
public class Test {
public static void main(String[] args) {
CountDownLatchDemo1 c1 = new CountDownLatchDemo1();
CountDownLatch countDownLatch = new CountDownLatch(4);
MyTaskCDL task = new MyTaskCDL(c1,countDownLatch);
for (int i = 0; i < 4; i++) {
// Thread thread = new Thread(task);
// thread.start();
new Thread(task).start(); //等同于上
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("理论值 = 400000000");
System.out.println("实际值" +c1.getCount());
System.out.println("理论和实际差 = " + (400000000 - c1.getCount()));
}
}
创建自增类对象 c1 , 创建CountDownLatch对象,并设计数器值为4.
最后创建自增任务类对象将两个c1和countDownLatch对象放入,因为自增任务类继承了Runnable
所有任务执行完毕,主线程执行打印任务
6. 线程通讯
6.1 等待唤醒机制
synchronized(内置锁)
- EntryList:双向队列:等待锁的多列
- Owner:正在执行的线程
- WaitSet:双向队列
-
- locko.wait:wait方法一定在synchronize代码块中,把当前线程放到waitset中
- locko.notify:把waitset队列中的一个随机线程放到entrylist中
- locko.notifyAll:把waitset队列中的所有线程都放到entrylist中
所有对象都会关联一个c++结构,这个结构ObjectMonier
在Java中,线程通信的等待唤醒机制通常使用 wait()
、notify()
和 notifyAll()
这三个方法来实现。
wait():使正在进行的线程进入等待状态,并释放锁,进入waitset
notify()notifyall():唤醒所有线程和其中一个线程
6.2 生产消费模式(包子铺案例)
包子铺:生产包子
吃货: 消费包子
包子对象
6.2.1 包子、包子铺、消费者
包子种类:
//定义皮和馅儿和包子的生产状态 flag
private String pier;
private String xianer;
private boolean flag = false;
public String getPier() {
return pier;
}
public void setPier(String pier) {
this.pier = pier;
}
public String getXianer() {
return xianer;
}
public void setXianer(String xianer) {
this.xianer = xianer;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public String toString() {
return "BaoZIDemo{" +
"pier='" + pier + '\'' +
", xianer='" + xianer + '\'' +
", flag=" + flag +
'}';
}
包子铺:
引用包子类,构造函数传参。 定义生产包子方法 。
方法里先初始化包子变量(定义count)。
当包子的setFlag返回true时候
包子铺 生产包子
public class BaoZITask extends Thread{
BaoZIDemo baozi;
public BaoZITask() {
}
public BaoZITask(BaoZIDemo baozi) {
this.baozi = baozi;
}
@Override
public void run() {
int count = 0;
while (true){
synchronized (baozi){
baozi.setFlag(true);;
System.out.println("包子熟了"+baozi);
System.out.println("吃货过来吃包子");
count++;
baozi.notify();
if (count % 2 == 0){
baozi.setPier("米皮");
baozi.setXianer("猪肉大葱");
}else {
baozi.setPier("面皮");
baozi.setXianer("韭菜鸡蛋");
}
if (baozi.isFlag()){
try {
baozi.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
消费者
public class XiaoFei extends Thread{
private BaoZIDemo baozi;
public XiaoFei() {
}
public XiaoFei(BaoZIDemo baozi) {
this.baozi = baozi;
}
@Override
public void run() {
while (true){
synchronized (baozi){
if (!baozi.isFlag()){
try {
baozi.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("吃货正在吃"+baozi);
baozi.setFlag(false);
baozi.notify();
}
}
}
}
测试类
public class Test {
public static void main(String[] args) {
BaoZIDemo baozi = new BaoZIDemo();
BaoZITask baizipu = new BaoZITask(baozi);
XiaoFei chihuo = new XiaoFei(baozi);
baizipu.start();
chihuo.start();
}
}
7. juc显示锁
JUC(java.util.concurrent)是Java标准库提供的用于并发编程的工具包。在JUC中,提供了一种显示锁的机制,用于更灵活地控制多个线程的访问。显示锁的主要类是ReentrantLock
,它实现了Lock
接口。synchronized 内置锁。 实例代码:
public class Example {
private Lock lock = new ReentrantLock();
public void doSomething() {
// 获取锁
lock.lock();
try {
// 执行需要同步的代码块
// ...
} finally {
// 释放锁
lock.unlock();
}
}
}
创建了一个ReentrantLock对象lock,它表示一个可重入的互斥锁。
在执行需要同步的代码块之前,调用lock.lock()方法获取锁,如果锁已经被
其他线程获取,则当前线程会进入等待状态。在执行完同步代码块之后,
一定要在finally块中调用lock.unlock()方法释放锁,
以确保锁一定会被释放,避免死锁的发生。
7.1 LOCK接口
Lock
接口是Java并发包(java.util.concurrent)中定义的一个接口,用于提供显示锁的功能。它提供了比隐式锁(synchronized关键字)更多的灵活性和功能扩展性。
常用方法:
void lock()获取锁:如果锁被其他线程获取,当前线程会被阻塞,知道获取到锁为止。
void unlock():释放锁,将持有的锁释放,并唤醒可能正在等待该锁的其他线程
boolean tryLock()
: 尝试获取锁。如果获取成功,则返回true;如果锁已经被其他线程获取,则返回false,不会阻塞当前线程。
boolean tryLock(long time, TimeUnit unit) throws InterruptedException
: 在指定的时间范围内尝试获取锁。如果在指定时间内成功获取锁,则返回true;如果超时仍未能获取锁,则返回false
7.2 ReentrantLock 基本用法
public class ReentrantLockExample {
private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new IncrementTask());
Thread t2 = new Thread(new IncrementTask());
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + count);
}
static class IncrementTask implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
lock.lock(); // 获取锁
try {
count++; // 对共享变量进行操作
} finally {
lock.unlock(); // 释放锁
}
}
}
}
}
-
首先,在
main
方法中创建了一个ReentrantLock
对象lock
,用于实现锁机制,并且定义了一个共享变量count
,初始值为0。 -
然后,定义了一个名为
IncrementTask
的内部类,实现了Runnable
接口,作为任务类。在IncrementTask
的run
方法中,用一个循环来进行累加操作。 -
在循环内部,第一步是通过
lock.lock()
方法获取锁。这样一来,如果其他线程已经获得了锁,则当前线程会被阻塞,直到获取到锁之后才能继续执行。 -
在获取到锁之后,执行
count++
操作,对共享变量count
进行累加。 -
最后,使用
lock.unlock()
方法释放锁。这样做的目的是确保在完成对共享变量的操作后,释放锁,以便其他线程能够获取锁并执行相同的操作。 -
在
main
方法中,创建了两个线程t1
和t2
分别启动了IncrementTask
任务。这两个线程会同时对count
变量进行累加操作。 -
最后,通过调用
t1.join()
和t2.join()
方法,使主线程等待t1
和t2
线程执行完毕。然后打印出最终的count
值。
7.3 读写锁
- 允许多个线程同时对共享资源进行读取操作,提高并发性能。
- 在有线程写入数据时,独占地获取写锁,确保数据的一致性和正确性。
- 在读多写少的场景下,减少线程竞争,提高并发性能。
使用 ReentrantReadWriteLock
可以有效地解决多个线程同时读取共享资源时可能发生的竞态条件问题,从而提高程序的性能。与 ReentrantLock
不同的是,ReentrantReadWriteLock
支持读写分离,即多个线程可以同时读取共享资源,但只有一个线程可以写入共享资源,以提高效率。
public class ReadWriteLockExample {
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static int count = 0;
public static void main(String[] args) {
Thread t1 = new Thread(new WriteTask());
Thread t2 = new Thread(new ReadTask());
Thread t3 = new Thread(new ReadTask());
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Final count: " + count);
}
static class WriteTask implements Runnable {
@Override
public void run() {
lock.writeLock().lock(); // 获取写锁
try {
count++; // 对共享变量进行操作
System.out.println("Write thread: " + count);
} finally {
lock.writeLock().unlock(); // 释放写锁
}
}
}
static class ReadTask implements Runnable {
@Override
public void run() {
lock.readLock().lock(); // 获取读锁
try {
System.out.println("Read thread: " + count); // 读取共享变量
} finally {
lock.readLock().unlock(); // 释放读锁
}
}
}
}
7.4 Semaphore
1. Semaphore 的主要方法
- Semaphore(permits):构造一个 Semaphore 实例,初始化其管理的许可数量为 permits 参数值。
- acquire( ):尝试获取 1 个许可。而当前线程被中断,则会抛出 InterruptedException 异常并终止阻塞
- release( ):释放 1 个可用的许可
2. Semaphore 示例
假设有 10 个人在银行办理业务,只有 2 个工作窗口,使用 Semaphore 模拟银行排队,代码如下:
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
//线程池,用于多线程模拟测试
final CountDownLatch countDownLatch = new CountDownLatch(10);
//创建信号量,含有2个许可
final Semaphore semaphore = new Semaphore(2);
AtomicInteger index = new AtomicInteger(0);
//创建Runnable可执行实例
Runnable r = () ->
{
try
{
//抢占一个许可
semaphore.acquire(1);
//模拟业务操作: 处理排队业务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()) + ", 受理处理中...,服务号: " + index.incrementAndGet());
Thread.sleep(1000);
//释放一个信号
semaphore.release(1);
} catch (Exception e)
{
e.printStackTrace();
}
countDownLatch.countDown();
};
//创建4条线程
Thread[] tArray = new Thread[10];
for (int i = 0; i < 10; i++)
{
tArray[i] = new Thread(r, "线程" + i);
}
//启动4条线程
for (int i = 0; i < 10; i++)
{
tArray[i].start();
}
countDownLatch.await();
}
}
8. 线程池的原理与实战
Java 线程需要 JVM 和 操作系统配合完成大量的工作:
- 必须为线程堆栈分配和初始化大量内存块,其中包含至少 1M 的栈内存。
- 需要进行系统调用,以便在操作系统中创建和注册本地线程。
频繁创建和销毁线程非常低效。通过使用线程池可以提升性能,方便线程的管理。
上图为例,4个线程为核心线程,剩下两个为应急线程,下方还有阻塞队列,队列满阻塞写方法,当队列空时阻塞读方法,等待写入数据。
8.1 JUC的线程池架构
常用的线程池实现类为ThreadPoolExecutor和 ScheduledThreadPoolExecutor(继承自ThreadPoolExecutor)
-
ThreadPoolExecutor:
ThreadPoolExecutor
是一个通用的线程池实现类,通过调整核心线程数、最大线程数、任务队列等参数,可以有效控制并发线程的数量和资源消耗。它可以用于执行各种类型的任务,包括Runnable和Callable。主要构造函数:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
参数说明:
corePoolSize
:核心线程数,即线程池保持的最小活动线程数。maximumPoolSize
:最大线程数,线程池允许的最大并发线程数。keepAliveTime
:非核心线程的空闲超时时间,超过该时间没有任务可执行时,多余的线程会被销毁。unit
:keepAliveTime的时间单位。workQueue
:缓冲任务的阻塞队列,用于存储待执行的任务。
ThreadPoolExecutor
还提供了一些方法来动态调整线程池的参数,比如设置核心线程数、最大线程数、任务队列大小等。 -
ScheduledThreadPoolExecutor:
ScheduledThreadPoolExecutor
继承自ThreadPoolExecutor
,同时具备线程池和定时任务调度的功能。它可以在指定的延迟时间后或定时周期性地执行任务。ScheduledThreadPoolExecutor
增加了一些额外的方法,用于创建和调度延迟任务以及周期性任务。主要包括:schedule(Runnable command, long delay, TimeUnit unit)
:创建一个延迟执行的任务。schedule(Callable<V> callable, long delay, TimeUnit unit)
:创建一个延迟执行的任务,并且可以获取返回值。scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
:创建一个固定速率执行的周期性任务。scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
:创建一个固定延迟执行的周期性任务。
这样可以方便地在指定的时间间隔内执行定时任务,适用于需要按照一定时间间隔循环执行任务的场景。
无论是 ThreadPoolExecutor
还是 ScheduledThreadPoolExecutor
,它们都是通过线程池来管理和复用线程,从而提高了线程的利用率和应用程序的性能。使用线程池可以避免频繁创建和销毁线程的开销,并且可以限制线程的最大数量,防止资源耗尽。同时,它们还提供了一些方法来监控线程池的状态、处理异常等。
8.2 Executor 创建线程池
JUC 就是 java.util .concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入到 JDK,用于完成高并发、处理多线程的一个工具包。
方 法 名 | 功能简介 |
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newCachedThreadPool() | 创建一个不限制线程数量的线程池,任何提交的任务都将立即执 |
newScheduledThreadPool() | 创建一个可定期或者延时执行任务的线程池 |
8.2.1 NewSingleThreadExecutor 单线程化线程池
该方法用于创建一个“单线程化线程池”,也就是只有一条线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池,能保证所有任务按照指定顺序(如FIFO)执行。代码具体如下:
@Slf4j
public class ThreadPoolDemo {
public static final int SLEEP_GAP = 500;
public static final int MAX_TURN = 5;
//异步的执行目标类
public static class TargetTask implements Runnable {
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
public void run() {
log.info("任务:{} doing", taskName);
ThreadUtil.sleepMilliSeconds(SLEEP_GAP);
log.info("{} 运行结束.", taskName);
}
@Override
public String toString() {
return "TargetTask{" + taskName + '}';
}
}
//测试用例:只有一条线程的线程池
@Test
public void testSingleThreadExecutor() {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask());
}
ThreadUtil.sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
}
10:14:00.798 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:14:01.304 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:14:01.304 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:14:01.804 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.
10:14:01.804 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-3 doing
10:14:02.305 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-3 运行结束.
10:14:02.305 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-4 doing
10:14:02.807 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-4 运行结束.
10:14:02.807 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-5 doing
10:14:03.308 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-5 运行结束.
特点:此线程池中的任务按照提交的次序,顺序执行。
唯一现成的存活时间是无限的。
当线程中的唯一线程正在繁忙时,新提交的任务实例会进入内部的阻塞队列(无界阻塞队列)
8.2.2 固定数量线程池:newFixedThreanPool
@Test
public void testNewFixedThreadPool() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask());
}
ThreadUtil.sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
测试结果
10:29:06.186 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-1 doing
10:29:06.186 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-3 doing
10:29:06.186 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-2 doing
10:29:06.690 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-3 运行结束.
10:29:06.690 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-1 运行结束.
10:29:06.690 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-2 运行结束.
10:29:06.690 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-5 doing
10:29:06.690 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - 任务:task-4 doing
10:29:07.191 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-5 运行结束.
10:29:07.191 [pool-1-thread-3] INFO com.lxs.demo.a_thread.ThreadPoolDemo - task-4 运行结束.
5个任务会先进行三个,等三个任务执行完后最后两个任务执行。
空出一个线程时就会执行等待队列中的线程
8.2.3 可缓存线程池:newCachedThreadPool
//测试用例:“可缓存线程池”
@Test
public void testNewCacheThreadPool() {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
sleepSeconds(1000);
//关闭线程池
pool.shutdown();
}
特点:
- 在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
- “可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。
8.2.4 可调度线程池:ScheduledThreadPoolExecutor
继承ThreadPoolExecutor,实现了可调度功能的线程池,当然也具备了线程池的特性,同时也提供了用于调度任务的方法。
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
// 延迟任务
executor.schedule(new Task(), 5, TimeUnit.SECONDS);
// 周期性任务
executor.scheduleAtFixedRate(new Task(), 1, 3, TimeUnit.SECONDS);
try {
Thread.sleep(10000); // 主线程等待10秒钟,让定时任务有足够的时间执行
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown(); // 关闭线程池
}
static class Task implements Runnable {
@Override
public void run() {
System.out.println("Task executed at: " + System.currentTimeMillis());
}
}
}
上述代码创建了一个包含两个线程的 ScheduledThreadPoolExecutor
实例,并调度了两个任务:一个是延迟执行的任务,5秒后执行;另一个是周期性任务,每隔3秒执行一次。通过调用 schedule()
方法可以创建一个延迟任务,指定任务和延迟时间。而使用 scheduleAtFixedRate()
则可以创建一个周期性任务,指定任务、初始延迟时间和任务执行的周期。在示例中,主线程等待10秒钟,让定时任务有足够的时间执行。然后调用 shutdown()
方法关闭线程池。
使用可调度线程池可以方便地在指定的时间间隔内执行定时任务和周期性任务。它可以替代传统的 Timer
类,并提供更灵活、高效的任务调度机制。
代码总结:
public class ThreadPoolDemo {
CountDownLatch countDownLatch = new CountDownLatch(MAX_TURN);
public static final int SLEEP_GAP = 500;
public static final int MAX_TURN = 5;
//异步的执行目标类
public static class TargetTask implements Runnable {
CountDownLatch countDownLatch;
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
public TargetTask(CountDownLatch countDownLatch) {
this();
this.countDownLatch = countDownLatch;
}
public void run() {
System.out.println("任务:" + taskName + " doing");
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(taskName + " 运行结束.");
countDownLatch.countDown();
}
@Override
public String toString() {
return "TargetTask{" + taskName + '}';
}
}
//测试用例:只有一条线程的线程池
@Test
public void testSingleThreadExecutor() {
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// //关闭线程池
pool.shutdown();
}
//测试用例:只有3条线程固定大小的线程池
@Test
public void testNewFixedThreadPool() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
pool.shutdown();
}
/**
* CachedThredPool:线程数没有限制
* @throws InterruptedException
*/
@Test
public void testNewCachedThreadPool() throws InterruptedException {
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < MAX_TURN; i++) {
pool.execute(new TargetTask(countDownLatch));
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
pool.shutdown();
}
//测试用例:“可调度线程池”
@Test
public void testNewScheduledThreadPool() {
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(countDownLatch),
0, 5000, TimeUnit.MILLISECONDS);
}
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//关闭线程池
scheduled.shutdown();
}
@Test
public void testThreadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, //corePoolSize
4, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(2), new ThreadPoolExecutor.CallerRunsPolicy());//workQueue
for (int i = 0; i < 7; i++) {
final int taskIndex = i;
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " taskIndex = " + taskIndex );
try {
Thread.sleep(10000);
// Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(task);
}
while (true) {
//每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
System.out.printf("- activeCount: %d - taskCount: %d\r\n", executor.getActiveCount(), executor.getTaskCount());
ThreadUtil.sleepSeconds(1);
}
}
}
8.3 线程池的标准创建方式
大部分企业的开发规范,都会禁止使用快捷线程池,要求通过标准构造器 ThreadPoolExecutor 去构造工作线程池。要求使用ThreadPoolExecutor创建线程池
// 使用标准构造器,构造一个普通的线程池
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收;
int maximumPoolSize, // 线程数的上限;
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
BlockingQueue workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
- 核心和最大线程数量
corePoolSize 核心线程池数,maximumPoolSize 最大线程数。线程池规则如下:
(1)在线程池接收到的新任务,并且当前工作线程数少于corePoolSize时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求,直到线程数达到 corePoolSize。
(2)当前工作线程数多于 corePoolSize 数量,但小于 maximumPoolSize 数量,则仅当任务排队队列已满时,才会创建新线程。 通过设置 corePoolSize 和 maximumPoolSize 相同,可以创建一个固定大小的线程池。
(3)当 maximumPoolSize 被设置为无界值(如 Integer.MAX_VALUE)时,线程池可以接收任意数量的并发任务。
( 4 ) corePoolSize 和 maximumPoolSize 不 仅 能 在 线 程 池 构 造 时 设 置 , 也 可 以 使 用setCorePoolSize 和 setMaximumPoolSize 两个方法进行动态更改。
BlockingQueue
BlockingQueue(阻塞队列)的实例用于暂时接收到的异步任务,如果线程池的核心线程都在忙,则所接收到的目标任务,缓存在阻塞队列中。
8.4 线程池的任务调度流程
(1)当前工作线程数小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中取一个空闲线程。
(2)线程池中任务数大于核心线程池数,任务将被加入到阻塞队列中,一直到阻塞队列满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不是为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中取下一个任务,并开始其执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(应急线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize。如果线程池的线程总数超时 maximumPoolSize,则线程池会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
@org.junit.Test
public void testThreadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, //corePoolSize
100, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(100));//workQueue
for (int i = 0; i < 5; i++) {
final int taskIndex = i;
executor.execute(() ->
{
Print.tco("taskIndex = " + taskIndex);
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
while (true) {
Print.tco("- activeCount:" + executor.getActiveCount() +
" - taskCount:" + executor.getTaskCount());
sleepSeconds(1);
}
}
5个线程,2个核心线程,100个最大线程任务数,100秒空闲线程保存时间,100个阻塞队列。
7个任务,两个核心线程,2个阻塞队列,4个最大线程,2个应急线程。超出了6个线程位置,则最后一个任务执行线程池的拒绝策略。
8.5 线程池的拒绝策略
使用有界队列的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:
(1)线程池已经被关闭。
(2)工作队列已满且 maximumPoolSize 已满。
无 论 以 上 哪 种 情 况 任 务 被 拒 , 线 程 池 都 会 调 用 RejectedExecutionHandler 实 例 的rejectedExecution 方法。 RejectedExecutionHandler 是拒绝策略的接口, JUC 为该接口提供了以下几种实现:
- AbortPolicy:拒绝策略
- DiscardPolicy:抛弃策略
- DiscardOldestPolicy:抛弃最老任务策略
- CallerRunsPolicy:调用者执行策略
- 自定义策略
JUC 线程池拒绝策略的接口与类之间的关系图
(1) AbortPolicy
使用该策略时,如果线程池队列满了则新任务被拒绝,并且会抛出 RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。
(2) DiscardPolicy
该策略是 AbortPolicy 的 Silent(安静)版本,如果线程池队列满了,新任务会直接被丢掉,并且不会有任何异常抛出。
(3) DiscardOldestPolicy
抛弃最老任务策略,也就是说如果队列满了,会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除对头元素后再尝试入队。
(4) CallerRunsPolicy
调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
在以上的四种内置策略中,线程池默认的拒绝策略为 AbortPolicy,如果提交的任务被拒绝,线程池抛出 RejectedExecutionException 异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获 RejectedExecutionException 异常。
(5)自定义策略
如果以上拒绝策略都不符合需求,则可自定义一个拒绝策略,实现 RejectedExecutionHandler接口的 rejectedExecution 方法即可。
自定义拒绝策略的例子,代码如下:
//自定义拒绝策略
public static class CustomIgnorePolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 可做日志记录等
Print.tco(r + " rejected; " + " - getTaskCount: " + e.getTaskCount());
}
}
@org.junit.Test
public void testCustomIgnorePolicy() {
int corePoolSize = 2; //核心线程数
int maximumPoolSize = 4; //最大线程数
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
//最大排队任务数
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程工厂
ThreadFactory threadFactory = new SimpleThreadFactory();
//拒绝和异常策略
RejectedExecutionHandler policy = new CustomIgnorePolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime, unit,
workQueue,
threadFactory,
policy);
// 预启动所有核心线程
pool.prestartAllCoreThreads();
for (int i = 1; i <= 10; i++) {
pool.execute(new TargetTask());
}
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}
8.6 线程池状态
线程池的 5 种状态,具体如下:
(1) RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。 (3) STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4) TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated( )钩子方法。 (5) TERMINATED:执行完terminated( )钩子方法之后的状态。
线程池的状态转换规则为:
(1)线程池创建之后状态为 RUNNING。
(2)执行线程池的 shutdown 实例方法,会使线程池状态从 RUNNING 转变为 SHUTDOWN。
(3)执行线程池的 shutdownNow 实例方法,会使线程池状态从 RUNNING 转变为 STOP。
(4)当线程池处于 SHUTDOWN 状态,执行器 shutdownNow 方法,会将其状态转变为 STOP状态。
(5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从 STOP 转变为TIDYING。
(6)执行完 terminated( ) 钩子方法之后,线程池状态从 TIDYING 转变为 TERMINATED 。