多线程-初阶
本节目标
- 认识多线程
- 掌握多线程程序的编写
- 掌握多线程的状态
- 掌握什么是线程不安全及解决思路
- 掌握 synchronized、volatile 关键字
1. 认识线程(Thread)
1.1 概念
- 线程是什么
还是回到我们之前的银行的例子中。之前我们主要描述的是个人业务,即一个人完全处理自己的
业务。我们进一步设想如下场景:
一家公司要去银行办理业务,既要进行财务转账,又要进行福利发放,还得进行缴社保。
如果只有张三一个会计就会忙不过来,耗费的时间特别长。为了让业务更快的办理好,张三又找
来两位同事李四、王五一起来帮助他,三个人分别负责一个事情,分别申请一个号码进行排队,
自此就有了三个执行流共同完成任务,但本质上他们都是为了办理一家公司的业务。
此时,我们就把这种情况称为多线程,将一个大任务分解成不同小任务,交给不同执行流就分别
排队执行。其中李四、王五都是张三叫来的,所以张三一般被称为主线程(Main Thread)。
- 为啥要有线程
首先, “并发编程” 成为 “刚需”.
- 单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. 而并发编程能更充分利用多核 CPU资源.
- 有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程.
其次, 进程池虽然能解决上述问题,提高效率,同时也有问题.池子里的闲置进程,不使用的时候也在消耗系统资源.消耗的系统资源太多了。虽然多进程也能实现 并发编程, 但是线程比进程更轻量.
- 创建线程比创建进程更快.
- 销毁线程比销毁进程更快.
- 调度线程比调度进程更快.
最后, 线程虽然比进程轻量, 但是人们还不满足, 于是又有了 “线程池”(ThreadPool) 和 “协程”(Coroutine)。关于线程池我们后面再介绍. 关于协程的话题我们此处暂时不做过多讨论.
- 进程和线程的区别
- 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。一个进程里可以有一个线程,也可以有多个线程.
- 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
- 进程是系统分配资源的最小单位,线程是系统调度的最小单位
- 只是创建进程的第一个线程的时候(由于要分配资源)成本是相对高的,后续这个进程中再创建其他线程,这个时候成本都是要更低一些,不必再分配资源了~
- 进程具有独立性.每个进程有各自的虚拟地址空间.一个进程挂了,不会影响到其他进程.同一个进程中的多个线程,共用同一个内存空间,一个线程挂了,可能影响到其他线程的,甚至导致整个进程崩溃~
- Java 的线程 和 操作系统线程的关系
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对用户层提供了一些 API 供用户使用(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装.
1.2 第一个多线程程序
感受多线程程序和普通程序的区别:
- 每个线程都是一个独立的执行流
- 多个线程之间是 “并发” 执行的.
import java.util.Random;
public class ThreadDemo {
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();
MyThread t2 = new MyThread();
MyThread t3 = new MyThread();
t1.start();
t2.start();
t3.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();
}
}
}
}
Thread-0
Thread-0
Thread-2
Thread-1
Thread-2
Thread-1
Thread-0
Thread-2
main
main
Thread-2
Thread-1
Thread-0
Thread-1
main
Thread-2
Thread-2
......
使用 jconsole
命令观察线程
1.3 创建线程
方法1 继承 Thread 类
- 继承 Thread 来创建一个线程类.
class MyThread extends Thread {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 MyThread类的实例
MyThread t = new MyThread();
- 调用 start 方法启动线程
t.start(); // 线程开始运行
方法2 实现 Runnable 接口
- 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("这里是线程运行的代码");
}
}
- 创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread t = new Thread(new MyRunnable());
- 调用 start 方法
t.start(); // 线程开始运行
对比上面两种方法:
- 继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
- 实现 Runnable 接口, this 表示的是
MyRunnable
的引用. 需要使用Thread.currentThread()
其他变形
- 匿名内部类创建 Thread 子类对象
// 使用匿名类创建 Thread 子类对象
Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("使用匿名类创建 Thread 子类对象");
}
};
- 匿名内部类创建 Runnable 子类对象
// 使用匿名类创建 Runnable 子类对象
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用匿名类创建 Runnable 子类对象");
}
});
- lambda 表达式创建 Runnable 子类对象
// 使用 lambda 表达式创建 Runnable 子类对象
Thread t3 = new Thread(() -> {
System.out.println("使用匿名类创建 Thread 子类对象");
});
1.4 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
举个例子:
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。
- 使用
System.nanoTime()
可以记录当前系统的 纳秒 级时间戳. serial
串行的完成一系列运算.concurrency
使用两个线程并行的完成同样的运算.- int begain = System.CurrentTimeMills; // (此种方法也可以)
- int end = System.CurrentTimeMills;
- int time = end - begain;
二者区别:
(1)System.nanoTime()的精确度更高一些,如今的硬件设备性能越来越好,如果要更精密计算执行某行代码或者某块代码所消耗的时间,该方法会测量得更精确。开发者可以根据需要的精确度来选择用哪一个方法。
(2)单独获取System.nanoTime()没有什么意义,因为该值是随机的,无法表示当前的时间。如果要记录当前时间点,用System.currentTimeMills()。
(3)System.currentTimeMills()得到的值能够和Date类方便地转换(在JDBC数据库中,使用java.mysql.timestamp进行转换为Date类型),jdk提供了多个接口来实现;但是System.nanoTime()则不行。
(4) System.currentTimeMills()的值是基于系统时间的,可以人为地进行修改;而System.nanoTime()则不能,所以如文章开头笔者碰到的问题一样,如果需要根据时间差来过滤某些频繁的操作,用System.nanoTime()会比较合适。
public class ThreadAdvantage {
// 多线程并不一定就能提高速度,可以观察,count 不同,实际的运行效果也是不同的
private static final long count = 10_0000_0000;
public static void main(String[] args) throws InterruptedException {
// 使用并发方式
concurrency();
// 使用串行方式
serial();
}
private static void concurrency() throws InterruptedException {
long begin = System.nanoTime();
// 利用一个线程计算 a 的值
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a--;
}
}
});
thread.start();
// 主线程内计算 b 的值
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
// 等待 thread 线程运行结束
thread.join();
// 统计耗时
long end = System.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("并发: %f 毫秒%n", ms);
}
private static void serial() {
// 全部在主线程内计算 a、b 的值
long begin = System.nanoTime();
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.nanoTime();
double ms = (end - begin) * 1.0 / 1000 / 1000;
System.out.printf("串行: %f 毫秒%n", ms);
}
}
并发: 399.651856 毫秒
串行: 720.616911 毫秒
2. Thread 类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
用我们上面的例子来看,每个执行流,也需要有一个对象来描述,类似下图所示,而 Thread 类的对象就是用来描述一个线程执行流的,JVM 会将这些 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 方法是否运行结束了
- 线程的中断问题,下面我们进一步说明
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + ": 我还
活着");
Thread.sleep(1 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": 我即将死去");
});
System.out.println(Thread.currentThread().getName()
+ ": ID: " + thread.getId());
System.out.println(Thread.currentThread().getName()
+ ": 名称: " + thread.getName());
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
System.out.println(Thread.currentThread().getName()
+ ": 优先级: " + thread.getPriority());
+ System.out.println(Thread.currentThread().getName()
+ ": 后台线程: " + thread.isDaemon());
System.out.println(Thread.currentThread().getName()
+ ": 活着: " + thread.isAlive());
System.out.println(Thread.currentThread().getName()
+ ": 被中断: " + thread.isInterrupted());
thread.start();
while (thread.isAlive()) {}
System.out.println(Thread.currentThread().getName()
+ ": 状态: " + thread.getState());
}
}
2.3 启动一个线程-start()
之前我们已经看到了如何通过覆写 run 方法创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
package thread;
public class Demo9 {
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
// t.run();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
只是调用t.run()方法,就会按照顺序执行,没有创建多线程
调用t.start()方法,就会创建多线程t,和main线程并发执行
注意:虽然是并发执行,但是第一行大概率是打印hello main,本质上main线程和 t 子线程是同时并发并行的执行,打印的顺序是随机的。但子线程需要申请系统运行及时间片轮转调度,而main线程一直处于运行态,从概率上看main先执行的几率非常大。
调用 start 方法, 才真的在操作系统的底层创建出一个线程.
2.4 中断一个线程
李四一旦进到工作状态,他就会按照行动指南上的步骤去进行工作,不完成是不会结束的。但有时我们需要增加一些机制,例如老板突然来电话了,说转账的对方是个骗子,需要赶紧停止转账,那张三该如何通知李四停止呢?这就涉及到我们的停止线程的方式了。
目前常见的有以下两种方式:
- 通过共享的标记来进行沟通
- 调用 interrupt() 方法来通知
示例-1: 使用自定义的变量来作为标志位.
- 需要给标志位上加 volatile 关键字(这个关键字的功能后面介绍).
public class ThreadDemo {
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: 使用 Thread.interrupted() //静态方法
或者 Thread.currentThread().isInterrupted()//实例方法
代替自定义标志位.
Thread 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.
- 使用 thread 对象的
interrupted()
方法通知线程结束.
public class ThreadDemo {
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 可以通过
1. Thread.interrupted() 判断当前线程的中断标志被设置,清除中断标志
2. Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种方式通知收到的更及时,即使线程正在 sleep 也可以马上收到。
示例-3 观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".
使用 Thread.isInterrupted() , 线程中断会清除标志位
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.interrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 只有一开始是 true,后边都是 false,因为标志位被清
false
false
false
false
false
false
false
false
false
使用 Thread.currentThread().isInterrupted()
, 线程中断标记位不会清除.
public class ThreadDemo {
private static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable target = new MyRunnable();
Thread thread = new Thread(target, "李四");
thread.start();
thread.interrupt();
}
}
true // 全部是 true,因为标志位没有被清
true
true
true
true
true
true
true
true
true
2.5 等待一个线程-join()
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
public class ThreadDemo {
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("王五工作结束了");
}
}
大家可以试试如果把两个 join 注释掉,现象会是怎么样的呢?
附录
带long millis的构造方法的解释:
因为join(2000)等待2s钟,但是t还没执行完,main停止等待,和t一起并发执行。t线程并不会结束运行,因为现在写的代码都是前台线程所有的执行完才会结束。
注意:关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
示例:
有三个线程,线程名称分别为:a,b,c。
每个线程打印自己的名称。
需要让他们同时启动,并按 c,b,a的顺序打印
public class dfg123 {
public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},"c");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
},"b");
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
},"a");
t1.start();
t2.start();
t3.start();
}
}
关于 join 还有一些细节内容,我们留到下面再讲解。
2.6 获取当前线程引用
这个方法我们以及非常熟悉了
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
2.7 休眠当前线程
也是我们比较熟悉一组方法,有一点要记得,因为线程的调度是不可控的,所以,这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println(System.currentTimeMillis());
Thread.sleep(3 * 1000);
System.out.println(System.currentTimeMillis());
}
}
关于 sleep,以后我们还会有一些知识会给大家补充。
2.8 sleep和join的对比(面试题)
sleep是Thread的静态方法,作用是使当前线程“睡眠”一定时间,期间线程不会释放对象锁。
join是Thread的普通方法,作用是等待调用join方法的线程死亡,才能接着执行当前线程的后续方法。
join和sleep从使用效果上来看,都能使线程处于“阻塞”状态
两者的主要区别
- sleep睡眠期间不会释放对象锁,像一个占有欲很强的小孩,睡觉了还死死地抱着布偶娃娃。
- join因为内部实现使用了wait方法,在等待的过程中所持有的对象锁会被释放。
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);
}
}
}
1. NEW(创建)
创建态:当一个已经被创建的线程处于未被启动时,即:还没有调用start方法时,就处于这个状态。(Thread对象有了,内核中的线程还没有)
2. RUNNABLE(运行时)
运行态:当线程已被占用,在Java虚拟机中正常执行时,就处于此状态。又可以分成正在工作中和即将开始工作.(内核中的线程没了,Thread对象还在)
3. BLOCKED(排队时)
阻塞态:当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态。当该线程持有锁时,该线程将自动变成RUNNABLE状态。
4. WAITING(休眠)
休眠态:一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。
5. TIMED_WAITING (指定休眠时间)
指定时间休眠态:基本同WAITING状态,多了个超时参数,调用对应方法时线程将进入TIMED_WAITING状态,这一状态将一直保持到超时期满或者接收到唤醒通知,带有超时参数的常用方法有Thread.sleep。
6. TERMINATED (结束)
结束态:从RUNNABLE状态正常退出而死亡,或者因为没有捕获的异常终止了RUNNABLE状态而死亡。
3.2线程状态和状态转移的意义
大家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
还是我们之前的例子:
刚把李四、王五找来,还是给他们在安排任务,没让他们行动起来,就是 NEW状态;
当李四、王五开始去窗口排队,等待服务,就进入到 RUNNABLE状态。该状态并不表示已经被银行工作人员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为一些事情需要去忙,例如需要填写信息、回家取证件、发呆一会等等时,进入BLOCKED、 WATING、 TIMED_WAITING状态,至于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED状态。
所以,之前我们学过的 isAlive()方法,可以认为是处于不是 NEW和 TERMINATED的状态都是活着的。
3.3观察线程的状态和转移
观察 1:关注 NEW、 RUNNABLE、 TERMINATED状态的转换
- 使用 isAlive方法判定线程的存活状态.
public class ThreadStateTransfer {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for (int i = 0; i < 1000_0000; i++) {
}
}, "李四");
System.out.println(t.getName() + ": " + t.getState());;
t.start();
while (t.isAlive()) {
System.out.println(t.getName() + ": " + t.getState());;
}
System.out.println(t.getName() + ": " + t.getState());;
}
}
观察 2:关注 WAITING、 BLOCKED、 TIMED_WAITING状态的转换
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("hehe");
}
}
}, "t2");
t2.start();
}
使用 jconsole可以看到 t1的状态是 TIMED_WAITING , t2的状态是 BLOCKED
修改上面的代码,把 t1中的 sleep换成 wait
public static void main(String[] args) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
try {
// [修改这里就可以了!!!!!]
// Thread.sleep(1000);
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "t1");
...
}
使用 jconsole可以看到 t1的状态是 WAITING
结论:
- BLOCKED表示等待获取锁, WAITING和 TIMED_WAITING表示等待其他线程发来通知.
- TIMED_WAITING线程在等待唤醒,但设置了时限; WAITING线程在无限等待唤醒
观察-3: yield()大公无私,让出 CPU
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
// 先注释掉, 再放开
// Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
可以看到:
- 不使用 yield的时候,张三李四大概五五开
- 使用 yield时,张三的数量远远少于李四
结论:
yield不改变线程的状态,但是会重新去排队.
4.多线程带来的的风险-线程安全 (重点)
4.1观察线程不安全
static class Counter {
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);
}
大家观察下是否适用多线程的现象是否一致?同时尝试思考下为什么会有这样的现象发生呢?
4.2线程安全的概念
想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
4.3线程不安全的原因
产生线程不安全的原因~
1. 线程是抢占式执行,线程间的调度充满随机性.[线程不安全的万恶之源!!]虽然这是根本原因,但是咱们无可奈何~
2. 多个线程对同一个变量进行修改操作。
(如果是多个线程针对不同的变量进行修改,没事!如果多个线程针对同一个变量读,也没事!)可以通过调整代码结构,使不同线程操作不同变量~
共享变量一般在堆上.因此可以被多个线程共享访问.
3. 针对变量的操作不是原子的~
原子性:一个操作不能被打断,要么全部执行完毕,要么不执行。
针对有些操作,比如读取变量的值,只是对应一条机器指令,此时这样的操作本身就可以视为是原子的。通过加锁操作,也就是把好几个指令给打包成一个原子的了。加锁操作,就是把这里的多个操作打包成—个原子的操作。
4. 内存可见性,也会影响到线程安全!!
可见性指,一个线程对共享变量值的修改,能够及时地被其他线程看到.
一个具体的栗子。针对同一个变量,一个线程进行读操作(循环进行很多次),一个线程进行修改操作(合适的时候执行一次)。读取内存操作,相比于读取寄存器,是一个非常低效的操作!!!(慢3-4个数量级)。而且如果t2线程迟迟不修改, t1线程读到的值又始终是一样的值!!因此, t1就有了一个大胆的想法!!!就会不再从内存读数据了,而是直接从寄存器里读。(不执行load 了)一旦t1做出了这种大胆的假设,此时万一t2修改了count值, t1就不能感知到了。这是Java编译器进行代码优化产生的效果~~
5. 指令重排序
咱们写的很多代码,彼此的顺序,谁在前谁在后无所谓。
编译器就会智能的调整这里代码的前后顺序从而提高程序的效率
保证逻辑不变的前提,再去调整顺序。
如果代码是单线程的程序,编译器的判定一般都是很准。
但是如果代码是多线程的,编译器也可能产生误判~
4.4解决之前的线程不安全问题
这里用到的机制,我们马上会给大家解释。
static class Counter {
public int count = 0;
synchronized 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);
}
线程安全问题的解决方案(2个):
方式一:对代码块加锁
格式:synchronize(锁对象){
需要被同步的代码
}
同步代码块需要注意的事项:
- 锁对象可以是任意的一个对象;
- 一个线程在同步代码块中sleep了,并不会释放锁对象;
- 如果不存在线程安全问题,千万不要使用同步代码块;
- 锁对象必须是多线程共享的一个资源,否则锁不住。
方式二:对方法或者类加锁
修饰符 synchronized 返回值类型 函数名(形参列表..){
}
同步函数注意事项:
- 如果函数是一个非静态的同步函数,那么锁对象是this对象;
- 如果函数是静态的同步函数,那么锁对象是当前函数所属的类的字节码文件(class对象);
- 同步函数的锁对象是固定的,不能由自己指定。
5. synchronized关键字-监视器锁 monitor lock
5.1 synchronized的特性
1)互斥
synchronized会起到互斥效果,某个线程执行到某个对象的 synchronized中时,其他线程如果也执行到同一个对象 synchronized就会阻塞等待.
- 进入 synchronized修饰的代码块,相当于加锁
- 退出 synchronized修饰的代码块,相当于解锁
synchronized用的锁是存在Java对象头里的。
理解 “阻塞等待”.
针对每一把锁,操作系统内部都维护了一个等待队列.当这个锁被某个线程占有的时候,其他线程尝试进行加锁,就加不上了,就会阻塞等待,一直等到之前的线程解锁之后,由操作系统唤醒一个新的线程,再来获取到这个锁.
注意:
- 上一个线程解锁之后,下一个线程并不是立即就能获取到锁.而是要靠操作系统来 “唤醒”.这也就是操作系统线程调度的一部分工作.
- 假设有 A B C三个线程,线程 A先获取到锁,然后 B尝试获取锁,然后 C再尝试获取锁,此时 B和 C都在阻塞队列中排队等待.但是当
A释放锁之后,虽然 B比 C先来的,但是 B不一定就能获取到锁,而是和 C重新竞争,并不遵守先来后到的规则.
synchronized的底层是使用操作系统的mutex lock实现的.
2)刷新内存
synchronized的工作过程:
- 获得互斥锁
- 从主内存拷贝变量的最新副本到工作的内存
- 执行代码
- 将更改后的共享变量的值刷新到主内存
- 释放互斥锁
所以 synchronized也能保证内存可见性.具体代码参见后面 volatile部分.
3)可重入
synchronized同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题;
理解 “把自己锁死”
一个线程没有释放锁,然后又尝试再次加锁.
// 第一次加锁, 加锁成功
lock();
// 第二次加锁, 锁已经被占用, 阻塞等待.
lock();
按照之前对于锁的设定,第二次加锁的时候,就会阻塞等待.直到第一次的锁被释放,才能获取到第二个锁.但是释放第一个锁也是由该线程来完成,结果这个线程已经躺平了,啥都不想干了,也就无法进行解锁操作.这时候就会死锁.
这样的锁称为不可重入锁.
synchronized
是可重入锁,因此没有上面的问题.
在可重入锁的内部,包含了 "线程持有者"和 "计数器"两个信息.
- 如果某个线程加锁的时候,发现锁已经被人占用,但是恰好占用的正是自己,那么仍然可以继续获取到锁,并让计数器自增.
- 解锁的时候计数器递减为 0的时候,才真正释放锁. (才能被别的线程获取到)
4)补充特性
- 开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁.(根据锁竞争的激烈程度,自适应)
- 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁.(如果锁冲突比较严重,就会变成重量级锁)(根据锁竞争的激烈程度,自适应)
- 实现轻量级锁的时候大概率用到的自旋锁策略,重量级锁的部分基于挂起等待锁来实现
- 是一种不公平锁
- 是一种可重入锁
- 是一种普通的互斥锁,不是读写锁
5.2 synchronized使用示例
synchronized本质上要修改指定对象的 “对象头”.从使用角度来看, synchronized也势必要搭配一个具体的对象来使用.
1)直接修饰普通方法:锁的 SynchronizedDemo对象
public class SynchronizedDemo {
public synchronized void methond() {
}
}
2)修饰静态方法:锁的 SynchronizedDemo类的对象
public class SynchronizedDemo {
public synchronized static void method() {
}
}
3)修饰代码块:明确指定锁哪个对象.
锁当前对象
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
锁类对象
public class SynchronizedDemo {
public void method() {
synchronized (SynchronizedDemo.class) {
}
}
}
我们重点要理解,synchronized锁的是什么.两个线程竞争同一把锁,才会产生阻塞等待.
两个线程分别尝试获取两把不同的锁,不会产生竞争.
5.3 Java标准库中的线程安全类
Java标准库中很多都是线程不安全的.这些类可能会涉及到多线程修改共享数据,又没有任何加锁措施.
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
但是还有一些是线程安全的.使用了一些锁机制来控制,比如:
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer
- Stack
StringBuffer的核心方法都带有 synchronized
还有的虽然没有加锁,但是不涉及 “修改”,仍然是线程安全的
- String
6. volatile关键字
6.1 volatile能保证内存可见性
Java内存模型 (JMM):
Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果.
-
线程之间的共享变量存在主内存 (Main Memory)。"主内存"才是真正硬件角度的 “内存”
-
每一个线程都有自己的 “工作内存” (Working Memory)。 “工作内存”,则是指 CPU的寄存器和高速缓存
-
当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据.
-
当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,再同步回主内存.
此时修改线程1的工作内存中的值,线程2的工作内存不一定会及时变化.
1)初始情况下,两个线程的工作内存内容一致
2)一旦线程1修改了 a的值,此时主内存不一定能及时同步.对应的线程2的工作内存的 a的值也不一定能及时同步
这个时候代码中就容易出现问题.
1)为啥要这么麻烦的拷来拷去?
前面我们讨论内存可见性时说了,直接访问工作内存(实际是 CPU的寄存器或者 CPU的缓存),速度非常快,但是可能出现数据不一致的情况.加上 volatile ,强制读写内存.速度是慢了,但是数据变的更准确了。
6.2 volatile 对比 synchronized
- volatile是变量修饰符,而synchronized则作用于一段代码块或方法。
- volatile是线程同步的轻量级实现,只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值,显然synchronized要比volatile消耗更多资源。
- volatile标记的变量不会被编译器优化,可以禁止指令重排序;synchronized标记的变量可以被编译器优化。
- voltile不能保证原子性,但是保证内存可见性
synchronized既能保证原子性,也能保证内存可见性. - 多线程访问volatile不会发生阻塞;而synchronize会发生阻塞;
7. wait和 notify
由于线程之间是抢占式执行的,因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序.
球场上的每个运动员都是独立的 “执行流” ,可以认为是一个 “线程”.
而完成一个具体的进攻得分动作,则需要多个运动员相互配合,按照一定的顺序执行一定的动作,线程1先 “传球” ,线程2才能 “扣篮”.
完成这个协调工作,主要涉及到三个方法
- 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异常.
代码示例:观察wait()方法使用
public static void main(String[] args) throws InterruptedException {
Object object = new Object();
synchronized (object) {
System.out.println("等待中");
object.wait();
System.out.println("等待结束");
}
}
这样在执行到object.wait()之后就一直等待下去,那么程序肯定不能一直这么等待下去了。这个时候就需要使用到了另外一个方法唤醒的方法notify()。
7.2 notify()方法
notify方法是唤醒等待的线程.
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait状态的线程。(并没有 “先来后到”)
- 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁。
7.3 notifyAll()方法
- notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程.
- 虽然是同时唤醒多个线程,但是这多个线程需要竞争锁.
7.4 wait和 sleep的对比(面试题)
其实理论上 wait和 sleep完全是没有可比性的,因为一个是用于线程之间的通信的,一个是让线程阻塞一段时间,
唯一的相同点就是都可以让线程放弃执行一段时间.
当然为了面试的目的,我们还是总结下:
- wait需要搭配 synchronized使用. sleep不需要.
- wait是 Object的方法 sleep是 Thread的静态方法.
7.5 wait 和join的对比(面试题)
wait()方法
wait()方法的作用是让当前线程进入等待状态,wait()会与notify()和notifyAll()方法一起使用。
notify()和notifyAll()方法的作用是唤醒等待中的线程,notify()方法:唤醒单个线程,notifyAll()方法:唤醒所有线程。
join()方法
join()方法是等待这个线程结束,完成其执行。它的主要起同步作用,使线程之间的执行从“并行”变成“串行”。
也就是说,当我们在线程A中调用了线程B的join()方法时,线程执行过程发生改变:线程A,必须等待线程B执行完毕后,才可以继续执行下去。
wait()方法和join()方法的相似处
-
wait()和join()方法都用于暂停Java中的当前线程,进入等待状态。
-
在Java中都可以调用interrupt()方法中断wait()和join()的线程状态。
-
wait()和join()都是非静态方法。
-
wait()和join()都在Java中重载。wait()和join()没有超时,但接受超时参数。
wait()方法和join()方法之间的区别
- 存在不同的java包中(最明显的区别)
wait()方法需要在java.lang.Object类中声明;而,join()方法是在java.lang.Thread类中的普通方法。
- 使用目的不同
wait()方法用于线程间通信;而join()方法用于在多个线程之间添加排序,第二个线程需要在第一个线程执行完成后才能开始执行。
- 唤醒线程方面的区别
我们可以通过使用notify()和notifyAll()方法启动一个通过wait()方法进入等待状态的线程。但是我们不能打破join()方法所施加的等待,除非中断或者调用连接的线程已执行完了。
- 同步上下文(最重要的区别)
wait()方法必须从同步(synchronized)的上下文调用,即同步块或方法,否则会抛出IllegalMonitorStateException异常。
但,在Java中有或没有同步的上下文,我们都可以调用join()方法。
8.多线程案例
8.1单例模式
啥是设计模式?
软件开发中也有很多常见的 “问题场景”.针对这些问题场景,大佬们总结出了一些固定的套路.按照这个套路来实现代码,也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例,而不会创建出多个实例.
这一点在很多场景上都需要.比如 JDBC中的 DataSource实例就只需要一个.
单例模式具体的实现方式,分成 "饿汉"和 "懒汉"两种.
主要应用懒汉模式。
饿汉模式
类加载的同时,创建实例.
package thread;
// 通过 Singleton 这个类来实现单例模式. 保证 Singleton 这个类只有唯一实例
// 饿汉模式
class Singleton {
// 1. 使用 static 创建一个实例, 并且立即进行实例化.
// 这个 instance 对应的实例, 就是该类的唯一实例.
private static Singleton instance = new Singleton();
// 2. 为了防止程序猿在其他地方不小心的 new 这个 Singleton, 就可以把构造方法设为 private
private Singleton() {}
// 3. 提供一个方法, 让外面能够拿到唯一实例.
public static Singleton getInstance() {
return instance;
}
}
public class Demo19 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
// Singleton instance2 = new Singleton();
}
}
懒汉模式-多线程版(最终版)
以下代码在加锁的基础上,做出了进一步改动:
- 使用双重 if判定,降低锁竞争的频率.
- 给 instance加上了 volatile.
package thread;
// 实现单例模式 - 懒汉模式
class Singleton2 {
// 1. 就不是立即就初始化实例.
private static volatile Singleton2 instance = null;
// 2. 把构造方法设为 private
private Singleton2() {}
// 3. 提供一个方法来获取到上述单例的实例
// 只有当真正需要用到这个 实例 的时候, 才会真正去创建这个实例.
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Demo20 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
理解双重 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) 就知道实例已经创建了,从而不再尝试获取锁了.降低了开销.
8.2阻塞式队列
阻塞队列是什么
阻塞队列是一种特殊的队列.也遵守 "先进先出"的原则.
阻塞队列能是一种线程安全的数据结构,并且具有以下特性:
- 当队列满的时候,继续入队列就会阻塞,直到有其他线程从队列中取走元素.
- 当队列空的时候,继续出队列也会阻塞,直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”.这是一种非常典型的开发模型.
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在 "秒杀"场景下,服务器同一时刻可能会收到大量的支付请求.如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程).这个时候就可以把这些请求都放到一个阻塞队列中,然后再由消费者线程慢慢的来处理每个支付请求.这样做可以有效进行 “削峰”,防止服务器被突然到来的一波请求直接冲垮.
2)阻塞队列也能使生产者和消费者之间解耦.
比如过年一家人一起包饺子.一般都是有明确分工,比如一个人负责擀饺子皮,其他人负责包.擀饺子皮的人就是 “生产者”,包饺子的人就是 “消费者”.擀饺子皮的人不关心包饺子的人是谁(能包就行,无论是手工包,借助工具,还是机器包),包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行,无论是用擀面杖擀的,还是拿罐头瓶擀,还是直接从超市买的).
标准库中的阻塞队列
在 Java标准库中内置了阻塞队列.如果我们需要在一些程序中使用阻塞队列,直接使用标准库中的即可.
- BlockingQueue是一个接口.真正实现的类是 LinkedBlockingQueue.
- put方法用于阻塞式的入队列, take用于阻塞式的出队列.
- BlockingQueue也有 offer, poll, peek等方法,但是这些方法不带有阻塞特性.
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
queue.put("hello"); // 入队列
String s = queue.take(); // 出队列. 如果没有 put 直接 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();
}
8.3定时器
定时器
定时器是什么
定时器也是软件开发中的一个重要组件.类似于一个 “闹钟”.达到一个设定的时间之后,就执行某个指定好的代码.
定时器是一种实际开发中非常常用的组件.
比如网络通信中,如果对方 500ms内没有返回数据,则断开连接尝试重连.
比如一个 Map,希望里面的某个 key在 3s之后过期(自动删除).
类似于这样的场景就需要用到定时器.
标准库中的定时器
- 标准库中提供了一个 Timer类. Timer类的核心方法为 schedule .
- schedule包含两个参数.第一个参数指定即将要执行的任务代码,第二个参数指定多长时间之后执行 (单位为毫秒).
import java.util.Timer;
import java.util.TimerTask;
public class Demo23 {
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("hello timer");
}
}, 3000); //3秒之后打印“helo”
System.out.println("main");
}
}
8.4线程池
线程池是什么
虽然创建销毁线程比创建销毁进程更轻量,但是在频繁创建销毁线程的时候还是会比较低效.
线程池就是为了解决这个问题.如果某个线程不再使用了,并不是真正把线程释放,而是放到一个 "池子"中,下次如果需要用到线程就直接从池子中取,不必通过系统来创建了.
Executors(标准库中的线程池)
ExecutorService
和 Executors
代码示例:
ExecutorService
表示一个线程池实例.Executors
是一个工厂类,能够创建出几种不同风格的线程池.ExecutorService
的submit
方法能够向线程池中提交若干个任务.
ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
})
Executors创建线程池的几种方式
- newFixedThreadPool:创建固定线程数的线程池
- newCachedThreadPool:创建线程数目动态增长的线程池.
- newSingleThreadExecutor:创建只包含单个线程的线程池.
- newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令.是进阶版的 Timer.
Executors本质上是 ThreadPoolExecutor类的封装.
ThreadPoolExecutor
ThreadPoolExecutor提供了更多的可选参数,可以进一步细化线程池行为的设定.
ThreadPoolExecutor的构造方法
理解 ThreadPoolExecutor构造方法的参数
把创建一个线程池想象成开个公司.每个员工相当于一个线程.
-
corePoolSize:核心线程数(正式员工的数量)
-
maximumPoolSize:最大线程数(正式员工 +临时工的数目)
-
keepAliveTime:临时工允许的空闲时间.
-
TineUnit unit: keepaliveTime的时间单位,是秒,分钟,还是其他值.
-
threadFactory:创建线程的工厂,参与具体的创建线程工作.
-
BlockingQueue:传递任务的阻塞队列(线程池会提供一个submit方法让程序猿把任务注册到线程池中)
-
RejectedExecutionHandler:拒绝策略,如果任务量超出公司的负荷了接下来怎么处理.
- AbortPolicy():超过负荷,直接抛出异常.
- CallerRunsPolicy():调用者负责处理
- DiscardOldestPolicy():丢弃队列中最老的任务.
- DiscardPolicy():丢弃新来的任务.
代码示例:
ExecutorService pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS,
new SynchronousQueue<Runnable>(),
Executors.defaultThreadFactory(),
new
ThreadPoolExecutor.AbortPolicy());
for(int i=0;i<3;i++) {
pool.submit(new Runnable() {
@Override
void run() {
System.out.println("hello");
}
});
}
线程池的工作流程
虽然线程池的参数这么多,但是使用的时候最最重要的参数,还是第一组参数线程池中线程的个数!!!
有一个程序,这个程序要并发的/多线程的来完成一些任务w,如果使用线程池的话,这里的线程数设为多少合适?
正确做法:要通过性能测试的方式,找到合适的值~~
根据这里不同的线程池的线程数,来观察程序处理任务的速度和程序持有的CPU的占用率~
- 当线程数多了,整体的速度是会变快,但是CPU占用率也会高.
- 当线程数少了,整体的速度是会变慢,但是CPU占用率也会下降.
关闭线程池
可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池。他们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无响应中断的任务可能永远无法停止。
但是他们存在一定的区别
- shutdownNow 首先 将线程池的状态 设置为STOP,然后 尝试停止 所有 正在执行或暂停任务 的线程,并 返回等待执行任务的列表
- shutdown 只是 将线程池的状态 设置成 SHUTDOWN 状态,然后 中断 所有正在执行的任务
只要调用了这两个关闭方法的一个,isShutdown就会返回true。当所有的任务都关闭后,才表示线程池关闭成功,这时调用isTerminated方法会返回true
至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定执行完,则可以调用shutdownNow方法。