多线程
- 一、多线程的创建
- 1.1 方式一 继承Thread类
- 1.2 方式二 实现Runnable接口
- 1.3 方式二的化简(匿名内部类)
- 1.4 实现Callable接口(JDK5新增)
- 1.5 小节
- 二、Thread常用API
- 2.1 获取当前线程对象
- 2.2 获取/设置线程名称
- 2.3 Thread的构造器
- 2.4 Thread类的线程休眠方法
- 2.5 小节
- 三、线程安全问题
- 3.1 问题出现场景
- 3.2 面向对象模拟出线程安全问题
- 四、线程同步
- 4.1 如何保证线程安全
- 4.2 线程同步的核心思想-加锁
- 4.3 线程同步方式一:同步代码块
- 4.4 锁对象规范
- 4.5 线程同步方式二:同步方法
- 4.6 同步方法和同步代码块的区别
- 4.7 线程同步方式三:Lock锁(JDK5)
- 五、线程池
- 5.1 线程池的作用
- 5.2 ThreadPoolExecutor创建线程池
- 5.3 临时线程什么时候创建
- 5.4 什么时候执行任务拒绝策略
- 5.5 线程池处理流程
- 5.6 线程池处理Runnable任务
- 5.7 线程池处理Callable任务
- 5.8 Executors工具类创建线程池
- 5.9 Executors存在的问题
- 六、定时器
- 6.1 Timer实现
- 6.2 ScheduleExecutorService实现
- 七、并发并行、线程的生命周期
- 7.1 并发和并行
- 7.2 线程的生命周期
一、多线程的创建
1.1 方式一 继承Thread类
public class Create {
//通过继承Thread类
public static void main(String[] args) {
//3.new一个新的线程对象
Thread t1 = new MyThread();//多态
//4.调用start方法启动线程(最终执行的还是run方法) 思考:为什么不直接调用run方法?
t1.start();//主线程从进入main方法时就启动了 执行到这里 主线程把t1线程启动了
//5.主线程(main)里执行的任务
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
//1.定义一个线程类(这只是一个类 并不是线程对象) 继承Thread类
class MyThread extends Thread {
//2.重写Thread的run方法 定义这个线程以后要干啥
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}
}
为什么不直接调用run方法?
- 假如直接调用run方法 主线程执行到run的时候 就把它当成一个类的一个方法 不会开启一个新的线程 依然就是主线程往下执行 导致永远都是先跑完run方法 再继续执行主线程
- 而start方法 就告诉了操作系统 执行到我这 请CPU把它当做一个单独的执行流程 以线程的方式来启动run方法
1.2 方式二 实现Runnable接口
- 这种实现接口的方式 将来就可以继承其他类 优化了方式一单继承的确定
public class Create02 {
public static void main(String[] args) {
//3.实例化一个[任务对象]
Runnable myRunnable = new MyRunnable();//多态
//4.把一个[任务对象]交给一个[线程对象]来处理
//调用Thread类的构造器 Thread(Runnable target)
//new Thread(myRunnable).start();
Thread t = new Thread(myRunnable);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
//1.定义一个线程[任务类] 实现Runnable接口
class MyRunnable implements Runnable {
//2.重写run方法 编写线程要完成的任务
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}
}
1.3 方式二的化简(匿名内部类)
public class test01 {
public static void main(String[] args) {
//实例化一个[任务对象]
Runnable myRunnable = new Runnable() {
//直接new Runnable的匿名对象
//右边这一坨 就相当于Runnable的一个[实现类对象]
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}
};
Thread t = new Thread(myRunnable);
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
化简一下:
public class test01 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
//new Thread的构造器 传进来一坨Runnable的匿名对象
//然后这一整托就创建了一个线程对象
//这个线程对象 可以返回给某个变量 也可以直接作为一个对象来调用方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}
});
t.start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
进一步化简(匿名对象):
public class test01 {
public static void main(String[] args) {
new Thread(new Runnable() {
//这个线程对象 也可以直接作为一个对象来调用方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}
}).start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
Lambda表达式继续化简
public class test01 {
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("子线程执行中" + i);
}
}).start();
for (int i = 0; i < 10; i++) {
System.out.println("主线程执行中" + i);
}
}
}
1.4 实现Callable接口(JDK5新增)
- 为了优化前两种方式 重写的run方法不能返回结果的问题(有些业务场景 可能需要返回线程执行结果)
- 任务的执行结果 就是call方法执行完的结果
主要理解:
- Callable任务对象交给FutureTask对象 FutureTask对象(属于Runnable接口家族)就可以交给Thread线程对象了
- FutureTask的特有方法get会等待子线程任务跑完 再去读取它的返回结果
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class test02 {
public static void main(String[] args) {
//Callable任务对象交给FutureTask对象 FutureTask对象(属于Runnable接口家族)就可以交给Thread线程对象了
//3.创建Callable[任务对象]callable
Callable<String> callable = new MyCall(200);
//可以把callable这个任务 像之前一样直接给线程对象吗?-不可以 所以还需要一层封装-FutureTask
//Thread t = new Thread(call);//err [Thread构造器只能接Runnable类型的]
//4.把callable任务对象 交给FutureTask类的对象(FT的构造器能接Callable类型)
// FutureTask类 [实现]了RunnableFuture接口 RunnableFuture接口又[继承]了Runnable接口
// 所以FutureTask的对象 本质上是Runnable的一个对象 而Runnable就可以交给Thread了
// FutureTask的对象通过它的get方法 得到线程的执行结果
// get方法能追踪call方法 [只有等call方法跑完了(线程执行完毕了)] 才会去拿执行结果
// 不能用callable对象直接调用call方法 因为这样去调call方法的时候 并不能保证call方法已经跑完了
FutureTask<String> futureTask = new FutureTask<>(callable);
//Runnable ft = new FutureTask<>(callable);//多态 但是get是FT的[特有方法] 所以不能这么写
//5.把futureTask交给Thread线程处理
Thread t1 = new Thread(futureTask);
t1.start();
try {
//get方法拿到线程执行结果 最终找的就是call方法的返回结果
//当主线程运行到这 就要去拿子线程的结果了 这个时候call方法可能都没跑完
//但是别担心 [如果没执行完 这里的代码就会等待 直到线程跑完 才拿结果]
System.out.println(futureTask.get());
//所以这里不能直接调call() 它不会监测子线程任务有没有跑完
// System.out.println(callable.call());
} catch (Exception e) {
e.printStackTrace();
}
}
}
//1.定义一个任务类 实现Callable[泛型]接口
// 因为Callable里面的任务要返回结果 泛型规定返回结果的类型
class MyCall implements Callable<String> {
private int n;
public MyCall(int n) {
this.n = n;
}
//2.重写call方法 线程要完成什么任务
@Override
public String call() throws Exception {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += i;
}
return "子线程执行结果是" + sum;
}
}
1.5 小节
二、Thread常用API
2.1 获取当前线程对象
2.2 获取/设置线程名称
2.3 Thread的构造器
2.4 Thread类的线程休眠方法
2.5 小节
- 我们调start启动线程之后 run方法是线程自己去调用的
三、线程安全问题
3.1 问题出现场景
为什么会出现问题?
- 存在多线程并发
- 同时访问共享资源
- 存在修改资源的操作(读资源是不会出现线程安全问题的)
3.2 面向对象模拟出线程安全问题
下面是不断执行程序 可能出现的结果
这说明小明和小红两个线程同时去取钱
最后结果到底是谁先去取钱 谁的取钱动作更快 有没有在下一个人来取钱之后把余额更新这些都是不确定的
很容易引发线程安全问题!
四、线程同步
4.1 如何保证线程安全
上面的取钱之所以存在安全问题 就是因为多个线程去取钱发现钱都是够的
虽然ABCD很多人可以同时访问同一个账户去取钱 但是在取钱那一刻 开始排队 就没有问题了
4.2 线程同步的核心思想-加锁
怎么保证线程先后依次访问共享资源?(唯一的锁对象)
4.3 线程同步方式一:同步代码块
- 小明小红来取钱 可能有先后 那么就让先来的人解锁进去取钱 然后更新余额
等先来的人走了 就解锁 第二个人进来发现余额不足 - 就算是他俩真的在时间维度上都是同一时刻来的 也存在锁竞争的算法 一定也只会让一个人进去取钱
4.4 锁对象规范
思考:上面随意地用"唯一"这种字符串作为锁 合不合理?
虽然理论上是可以 但是会影响其他无关线程的执行
假如有另外一家人 小黑小白 来取钱
如果我用的是"唯一"这种锁
那么小黑小白在小明取钱的时候 也被锁在外面等
这显然不合理
应该一家人用一家人的锁才对
- 建议使用共享资源作为锁对象
- 对于实例方法 建议用this作为锁对象
- 对于静态方法 建议用字节码对象(类名.class)作为锁对象
4.5 线程同步方式二:同步方法
4.6 同步方法和同步代码块的区别
-
同步代码块锁的范围更小 性能会好一些
同步代码块相当于把厕所的坑锁起来了 多个线程可以都同时跑到厕所里面去 -
同步方法锁的范围更大 性能相对较差(但是可读性好 所以用的也多)
它相当于把整个厕所锁起来了 一起跑到厕所门口等 像之前的取钱案例 同步代码块还能一起先拿到name 同步方法只能在整个方法外面等着
4.7 线程同步方式三:Lock锁(JDK5)
小明 小红两个线程(用户) 对同一个账户来操作 拿的锁对象也是同一把
(一个账户对应一把锁)
五、线程池
5.1 线程池的作用
- 线程池就是一个可复用线程的技术 可以提高性能
- 工作线程也叫核心线程
- ExecutorService接口代表了线程池
- 任务队列里的任务 是通过任务接口Runnable和Callable创建的
- 不使用线程池的话 每次用户提交任务都新建线程 对于内存和CPU的开销都很大(java中 线程就是对象 对象存在内存)
5.2 ThreadPoolExecutor创建线程池
- 通过ExecutorService接口的实现类ThreadPoolExecutor自创一个线程池对象
- 使用Executors(线程池的工具类)调用API 返回不同特点的线程池对象
参数三指定的是临时线程的存活时间 也就是除去核心线程(长期工) 剩下的那些(临时工)
-
corePoolSize 核心线程数 当有新任务提交时会进行下面的判断
1.如果当前线程数(正在工作的线程)小于corePoolSize,则新建(核心)线程处理任务,即使此时有空闲线程也不使用
2.如果当前线程数(正在工作的线程)大于corePoolSize且小于maximumPoolSize(说明还能创建临时线程),如果workQueue未满,则加入workQueue,否则新建(临时)线程处理此任务
3.如果corePoolSize 和 maximumPoolSize相同,当workQueue未满时放入workQueue,否则按照拒绝策略处理新任务
4.如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务。 -
workQueue
假设10个最大线程数的线程都在忙 第11个任务提交的时候 如果任务队列没满 就进去这个任务队列缓存起来 -
keepAliveTime
临时线程空闲时间等待超过这个keepAliveTime时间才会被回收 -
handler 拒绝策略,分为下面几种
1.AbortPolicy,直接抛出异常,默认的拒绝策略;
2.CallerRunsPolicy,使用调用者所在的线程执行任务(绕过线程池 主线程亲自来服务);
3.DiscardOldestPolicy,丢弃队列中最前面的任务(等待最久的任务),并将当前任务加入到队列中;
4.DiscardPolicy,直接丢弃任务
本段参数说明原文链接
5.3 临时线程什么时候创建
核心线程都在忙;临时线程还能创建;任务队列也满了
这个时候提交新任务 就会创建临时线程(看5.2的核心线程第二点判断)
为什么要这么做?
这样其实尽量少创建了线程
因为核心线程都在忙 任务队列也没排满
核心线程可能马上就忙完了 就可以处理其他任务了
只有等任务队列都满了 还有新任务提交 这种极端情况 才要临时线程登场帮忙
如果核心线程都在忙 新任务来了直接就创建临时线程 资源利用率就低了
5.4 什么时候执行任务拒绝策略
如果运行的线程数量大于等于maximumPoolSize
这时如果workQueue已经满了
则通过handler所指定的策略来处理任务
5.5 线程池处理流程
5.6 线程池处理Runnable任务
import java.util.concurrent.*;
public class test05 {
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
//创建线程池对象
//三个核心线程池 最大线程数5 任务队列5 也就是最多处理5+5个任务
3,5,6,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable task = new MyThread001();
//到这里 每次都让一个核心线程来工作
pool.execute(task);
pool.execute(task);
pool.execute(task);
//当我加上sleep 上面三个还没跑完 到这里 这里的任务只会等待(如果任务不超过任务队列能接纳的5个)
pool.execute(task);
pool.execute(task);
pool.execute(task);
pool.execute(task);
pool.execute(task);
//再来一个 超过五个了 就要派出临时线程了(并不一定只创建一个)
//最对5-3=2个临时线程
pool.execute(task);
pool.execute(task);
// 核心线程都在忙
// 临时线程创建完了 也都在满
// 任务队列也满了
// 这个时候要执行拒绝策略了
pool.execute(task);
}
}
运行结果:
5.7 线程池处理Callable任务
- 使用ExecutorService接口提供的submit方法
- 同样这里的get方法会等任务跑完再取出结果
5.8 Executors工具类创建线程池
import java.util.concurrent.*;
public class test05 {
public static void main(String[] args) throws Exception{
//创建固定线程数量的线程池对象
ExecutorService pool = Executors.newFixedThreadPool(3);
pool.execute(new MyThread001());
pool.execute(new MyThread001());
pool.execute(new MyThread001());
// 这个任务会一直被阻塞 除非run里面没有sleep
// 那么三个核心线程应该能对付>3个任务
pool.execute(new MyThread001());
}
}
5.9 Executors存在的问题
六、定时器
6.1 Timer实现
- 定时器是一种控制任务延时调用 周期调用的技术
- TimerTask实现了Runnable接口
- 他最大的问题是单线程执行
import java.util.Timer;
import java.util.TimerTask;
public class Timer_ {
public static void main(String[] args) {
Timer timer = new Timer();//定时器本身就是一个单线程
//TimerTask task, long delay, long period
//这一坨代表一个匿名对象(千万不能看成[抽象类]TimerTask的实现类 但可以看成TimerTask的子类的对象)
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了一个任务");
}
},3000,2000);//3s后开始执行 每过2s执行一次
}
}
Timer存在的问题:
6.2 ScheduleExecutorService实现
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Timer_ {
public static void main(String[] args) {
//1.创建ScheduledExecutorService线程池 做为定时器
ScheduledExecutorService pool = Executors.newScheduledThreadPool(3);
//2.开启定时任务1
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
// System.out.println(10/0); //这个任务就算挂了 也不影响任务2
try {
System.out.println(Thread.currentThread().getName() + "在执行任务" + new Date());
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
}
}, 0, 2, TimeUnit.SECONDS);//不延迟 2s执行一次
//3.开启定时任务2 不会收到任务1的影响
pool.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "在执行任务" + new Date());
}
}, 0, 1, TimeUnit.SECONDS);//不延迟 2s执行一次
}
}
七、并发并行、线程的生命周期
7.1 并发和并行
- 正在运行的程序(软件 跑起来的java代码)就是一个独立的进程,线程是属于进程的
- 线程其实是CPU并发与并行同时执行的
- 并发就是CPU分时轮询执行线程 但是切换的速度贼快
- 并行是站在时间维度上 任何一个时刻 CPU最多在处理8个线程(8个取决于CPU有几个逻辑处理器)
假设我CPU的逻辑处理器是8 那么每次最多同时处理(并行)8个线程
但是计算机肯定不止处理8个线程
这时候就会并发执行:CPU一会处理这几个线程 一会又处理那几个线程 轮询提供服务 但是切换的速度极快 所以看上去似乎好多线程在同时执行 这就是并发
也就是说我这个8个逻辑处理器的CPU 在任何一个时刻/瞬间 最多都只会处理8个线程(这就是并行)
7.2 线程的生命周期
- 新建状态只有Java(对象)特征 没有线程特征
- 调用sleep()休眠之后 不会释放锁 小红就算在里面睡一天 小明也得在外面等着
- 调用wait()进入无限等待状态后 会立即释放锁
- wait(5000) 如果5s内一直没人唤醒我 我自己就醒了