多线程
线程的创建方式
- 继承Thread类,覆写run方法(线程的核心工作任务方法 )
- 覆写Runnable接口,覆写run方法
这两种方法的最终启动线程都是通过Thread类的start进行的
-
覆写Callable接口,覆写call方法
-
使用线程池创建
方式一:继承Thread类
- 创建一个子类继承Thread类
- 覆写run方法
- 产生当前这个子类对象,调用start方法启动
注意:继承Thread类的子类就是一个线程实体
/**
* 继承Thread类创建线程
*/
public class ThreadMethod extends Thread {
@Override
public void run(){
System.out.println("这是子线程的输出结果");
System.out.println("这是子线程的输出结果");
System.out.println("这是子线程的输出结果");
}
}
public class Main {
public static void main(String[] args) {
// 1. 创建线程类对象
ThreadMethod mt = new ThreadMethod();
// 2. 启动线程
mt.start();
System.out.println("这是Thread方式实现的主线程的输出语句");
}
}
方法二:覆写Runnable接口
- 创建一个子类实现Runnable接口
- 覆写run方法
- 产生当前这个子类的对象(即创建了一个任务对象)
- 创建线程Thread类对象
- 将子类对象传入线程对象(即向线程中添加一个任务)
- 调用start方法启动
注意:与继承Thread类不同的是,实现Runnable接口的子类并不是直接的线程对象,只是一个线程的核心工作任务。(说清线程的任务和线程实体的关系)
public class RunnableMethod implements Runnable{
@Override
public void run() {
System.out.println("这是Runnable方式实现的子线程任务");
}
}
public class Main {
public static void main(String[] args) {
// 1. 创建线程的任务对象
RunnableMethod runnableMethod = new RunnableMethod();
// 2. 创建线程对象,将任务对象传入线程对象
// 即创建 Thread 类实例, 调用 Thread 的构造方法时将 Runnable 对象作为 target 参数.
Thread thread = new Thread(runnableMethod);
// 3. 启动线程
thread.start();
System.out.println("这是Runnable方式实现的主线程的输出语句");
}
}
推荐用方式二,实现Runnable接口更灵活,子类还能实现别的接口,继承别的类。方式一只能继承Thread类(单继承局限)
注意:调用start方法启动线程,是由JVM产生操作系统的线程并启动,到底什么时候真正启动,对于我们来说不可见,也无法控制。即线程(主线程,子线程)是并行,没有先后顺序,只是代码看起来向串行
对比上面两种方法:
继承 Thread 类, 直接使用 this 就表示当前线程对象的引用.
实现 Runnable 接口, this 表示的是 MyRunnable 的引用. 需要使用 Thread.currentThread()
关于方式一二的不同写法
-
匿名内部类继承Thread类,然后实现run方法
// 匿名内部类继承Thread方法 Thread t1 = new Thread(){ @Override public void run() { System.out.println("匿名内部类继承Thread类"); // 打印线程名字 System.out.println(Thread.currentThread().getName()); } }; t1.start(); System.out.println("这是主线程"+Thread.currentThread().getName());
-
匿名内部类实现Runnable接口
Thread t2 = new Thread(new Runnable() { @Override public void run() { System.out.println("匿名内部类实现Runnable接口"); // 打印线程名字 System.out.println(Thread.currentThread().getName()); } }); t2.start(); System.out.println("这是主线程"+Thread.currentThread().getName());
-
lamada表达式实现Runnable接口
lamada表达式建立在函数式接口上,函数式接口只有一个抽象方法
Thread t3 = new Thread(()-> System.out.println("lamada表达式实现Runnable接口")); t3.start(); System.out.println("这是主线程"+Thread.currentThread().getName());
多线程的优势
优势:增加运行速度
可以观察多线程在一些场合下是可以提高程序的整体运行效率的。通过例子,感受一下多线程和顺序执行的速度差异
- 使用 System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
- serial 串行的完成一系列运算. concurrency 使用两个线程并行的完成同样的运算.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBXkpTtc-1661621920022)(img/image-20220702114402592.png)]
public class ThreadSpeedCompare {
private static final long COUNt = 10_0000_0000; // 下划线表分隔
public static void main(String[] args) throws InterruptedException {
serial();
concurrent();
}
// 串行进行20亿的累加
private static void serial() {
long start = System.nanoTime();
long a = 0;
for (long i = 0; i < COUNt; i++) {
a ++;
}
// b的执行需要等待a走完才能进行
long b = 0;
for (long i = 0; i < COUNt; i++) {
b ++;
}
long end = System.nanoTime();
double allTime = (end - start) * 1.0 / 1000 / 1000;
System.out.println("顺序执行共耗时 : " + allTime + "ms");
}
// 并发进行 + 10亿操作
private static void concurrent() throws InterruptedException {
long start = System.nanoTime();
// 子线程进行 + 10亿操作
Thread thread = new Thread(() -> {
long a = 0;
for (long i = 0; i < COUNt; i++) {
a ++;
}
});
thread.start();
// 主线程中也执行 + 10亿操作 (主线程所在地就是子线程对象被创建的地方)
long b = 0;
for (long i = 0; i < COUNt; i++) {
b ++;
}
// 等待子线程执行结束,即主线程和子线程的加法操作都完成
// join方法等待子线程thread执行结束才能执行下面代码
thread.join();
long end = System.nanoTime();
double allTime = (end - start) * 1.0 / 1000 / 1000;
System.out.println("并发执行共耗时 : " + allTime + "ms");
}
}
总结:多线程最大的应用场景就是把一个大任务拆分成多个子任务(交给子线程),多个子线程并发执行,提高系统处理效率
Thread类及常见方法
Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关联。
构造方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kVMnHoir-1661621920024)(img/image-20220702152437296.png)]
// 一般搭配子类使用,需要有一个继承了Thread类的子类
Thread t1 = new Thread();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("传入Runnable对象");
}
});
Thread t3 = new Thread("鹏哥线程");
// 最常用:创建线程对象的同时命名
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hehe");
}
},"铭哥线程");
Thread的核心属性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g6YyTWM3-1661621920024)(img/image-20220702155252469.png)]
-
ID 是线程的唯一标识,每个线程都有一个独立的id,不同线程不会重复(ID由JVM生成不会重复,name是人为设置可以重复)
-
名称是各种调试工具用到
-
状态表示线程当前所处的一个情况,下面我们会进一步说明
-
优先级高的线程理论上来说更容易被调度到(优先级越高的线程越有可能被CPU优先执行,但Java程序只是建议优先级高的线程优先执行,到底执行不执行,OS说了算)
-
关于后台线程,需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行。
-
是否存活,即简单的理解,为 run 方法是否运行结束了
-
线程的中断问题,下面进一步进行说明
Thread thread = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10; i++) { // 此时Thread.currentThread()指的是调用run的子线程对象 System.out.println(Thread.currentThread().getName()+"我还活着"); try{ Thread.sleep(1000); }catch (InterruptedException e){ throw new RuntimeException(e); } } System.out.println(Thread.currentThread().getName()+"I'm going to die..."); } }, "线程a"); // --------------------------------------以下为主线程 // 此时Thread.currentThread()指的是主线程 System.out.println(Thread.currentThread().getName()+": ID is "+Thread.currentThread().getId()); System.out.println("状态:"+Thread.currentThread().getState()); System.out.println("优先级:"+Thread.currentThread().getPriority()); System.out.println("是否是后台线程?"+Thread.currentThread().isDaemon()); System.out.println("主线程是否是存活?"+Thread.currentThread().isAlive()); System.out.println("子线程thread是否是存活?"+thread.isAlive()); thread.start();
启动线程
启动线程调用的是Thread类的start方法
注意:只有调用 start 方法, 才真的在操作系统的底层创建出一个线程.
通过覆写 run 方法可以创建一个线程对象,但线程对象被创建出来并不意味着线程就开始运行了。
- 覆写 run 方法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- 而调用 start() 方法,就是喊一声:”行动起来!“,线程才真正独立去执行了。
注意:同一多次调用start方法会抛出异常
中断线程
中断线程是线程间通信的一种方式。
普通线程会在run方法执行结束之后自动停止,而==中断线程指的是中断一个正在执行的线程(即run方法还没有执行结束)。==中断线程有两种方式:
1. 通过共享变量进行中断
Thread类的所有静态方法(如:sleep,interruppt等)都是在哪个线程中调用的,就生效在哪个线程
public class ThreadInterruptByVar {
private static class MyThread implements Runnable{
// 在多个线程都会用到的变量加上volatile关键字
volatile boolean isQuit = false;
@Override
public void run() {
while (!isQuit){
System.out.println(Thread.currentThread().getName() + "别烦我,我正在跳舞呢~");
try {
// sleep在子线程使用,中断子线程
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "被中断了~");
}
}
public static void main(String[] args) throws InterruptedException {
MyThread mt = new MyThread();
Thread thread = new Thread(mt,"Lay的线程");
System.out.println("Lay可以开始跳舞了~");
thread.start();
// sleep在主线程使用,中断主线程
// 主线程暂停3s,暂停thread
Thread.sleep(3000);
System.out.println("音响坏了,让Lay停下");
mt.isQuit = true;
}
}
2. 调用 interrupt() 方法来通知
-
thread . interrupt():用于修改指定线程为中断状态,调用 线程对象的interrupt() 方法就会将线程对象状态置为中断状态,并抛出一个异常通知。
-
Thread类的内部包含了一个属性, 指示当前线程是否被中断的属性。因此可以通过调用Thread.interrupted()和Thread.currentThread().isInterrupted()判断线程是否中断
线程接收内置中断通知方式
1)当线程因调用了sleep/wait/join等方法而处在阻塞状态时,收到中断通知 thread.interrupt() 后,就会抛出一个中断异常InterruptedException,清除中断标志。(只要抛出中断异常,就一定会清除中断,与使用Thread类中哪种方法判断中断无关)
- 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
例:
public class ThreadInterruptedByMethod {
private static class MyRunnble implements Runnable{
@Override
public void run() {
// 1.判断当前线程是否被中断了
// 1.1 静态方法,清除中断标志
// while (!Thread.interrupted()) {
// 1.2 成员方法,判断当前线程对象是否被中断了,不清除中断标志
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName()+"别烦我,正对线呢!");
try {
// 2. 判断线程是否中断后,仍调用sleep/wait/join方法
Thread.sleep(1000);
} catch (InterruptedException e) {
// 当线程被中断时,会抛出中断异常
// 抛出中断异常之后,中断标志一定会被清除!!!与调用 1or2哪种方法判断中断无关
// 若无break语句线程会继续执行
System.err.println("蛮子在偷家!!!!");
break;
}
}
System.out.println(Thread.currentThread().getName() + "糟了,水晶被偷了,凉了~");
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnble mt = new MyRunnble();
Thread thread = new Thread(mt,"VN线程");
System.out.println("VN正在对线");
thread.start();
Thread.sleep(5*1000);// 5秒
// 中断子线程
// 调用此方法就会将子线程的状态置为中断状态
thread.interrupt();
}
}
2)线程没有调用以上三种方法时,仅判断内部的中断标志是否被设置,可以通过:
- Thread.interrupted() 判断当前线程的中断标志是否被设置,若被设置了,清除中断标志 (即若此时返回true,在本次方法调用之后,还原中断状态为false)
- Thread.currentThread().isInterrupted() 判断指定线程的中断标志是否被设置,若被设置了,不清除中断标志
例:观察标志位是否清除
标志位是否清除, 就类似于一个开关.
Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 “清除标志位”
Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为"不清除标志位".
/**
* 两种中断方法的区别
**/
public class ThreadDoubleMethod {
private static class MyRun implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
// 若判断当前线程被中断,状态为true,返回当前状态并清除(置为false),在下一次循环时返回false
// System.out.println(Thread.interrupted());
// 若判断当前线程被中断,状态为true,仅返回当前状态,不清除,下一次循环仍返回true
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
public static void main(String[] args) {
MyRun myRun = new MyRun();
Thread thread = new Thread(myRun);
thread.start();
// 打断子线程
thread.interrupt();
}
}
等待一个线程
有时,我们需要等待一个线程完成它的工作后,才能进行自己的下一步工作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要一个方法明确等待线程的结束。
线程对象join():在某个线程A中调用别的线程对象B的join方法,意思就是这个线程A要等待另一个线程B执行完毕后,才能继续执行本线程A的后续代码。
获取当前正在执行的线程对象
Thread.cunrrentThead() =>获取CPU上正在执行的线程对象
说明
休眠当前线程
Thread.sleep(long millis):单位毫秒,在哪个线程里被调用,就休眠哪个线程
例:
/**
* 线程等待 - join方法 - 成员方法
**/
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "还在学习JavaSE阶段...");
try {
// 没有sleep,线程嗖的一下就执行完了,无法知道具体用时。
// 用sleep休眠一秒,使得线程运行用时可以被统计记录下来,进行精确控制
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"JavaSE线程");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + "进入数据结构的学习部分");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
},"数据结构线程");
System.out.println("先学习JavaSE");
t1.start();
// 主线程死等t1,直到t1执行结束主线程再恢复执行
t1.join();
// 此时走到这里,t1线程已经执行结束,再启动t2线程
t2.start();
// 1. main中 -> 调用t2.join() 阻塞主线程,直到t2完全执行结束再恢复主线程的执行
// 在某个线程A里,让另一个线程对象B调用join()方法,则该线程A被阻塞,必须等另一个线程B执行完毕后才恢复运行
// t2.join();
// 2. 主线程只等t2 2000ms - 2s,若t2在2s之内还没结束,主线程就会恢复执行
t2.join(2000);
// t2线程也执行结束了,继续执行主线程
System.out.println("开始学习JavaEE");
System.out.println(Thread.currentThread().getName());
}
}
线程的状态
/**
* 线程状态
*/
public class ThreadState {
public static void main(String[] args) {
for(Thread.State state: Thread.State.values()){
System.out.println(state);
}
}
}
状态类型
- NEW: 新建状态,安排了工作, 还未开始行动
- RUNNABLE: 可执行状态,是新建状态的下一个状态,可以分成正在运行(RUNNING)和即将开始执行(READY)。
- BLOCKED \ WAITING \ TIMED_WAITING:这三个状态都是等待状态,当前线程暂停执行,等待其他任务或资源,但造成的暂缓执行的原因不同:
- WAITING:lock.wait(),线程等待,等待被另一个线程唤醒(notify方法)
- TIMED_WAITING:超时等待,需要等待一段时间后自动唤醒
- BLOCKED:锁等待,需要等待其他线程释放锁对象
- TERMINATED: 终止状态,当前线程已经之行结束了,可以被销毁
以下三种状态都属于线程的阻塞状态(该线程需要暂缓执行,):
状态相关函数
-
thread.isAlive():判断线程是否存活。除了New和Terminated状态,都是存活状态
-
thread.getState():获取线程当前状态
- NEW: 新创建的线程对象就处于new状态
- RUNNABLE: 就绪和运行都是runnable状态
- TERMINATED:线程的run方法执行完毕,或者抛出异常不正常执行完毕都会进入终止状态
// 产生一个线程对象,该对象默认的状态就是新建状态 - NEW Thread t = new Thread(() -> { for (int i = 0; i < 100_000; 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());
synchronized 关键字-监视器锁monitor lock
synchronized 的特性
1) 互斥
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待.
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
synchronized用的锁是在Java对象里的。
可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 “锁定” 状态(类似于公共厕所的 “有人/无人”).
如果当前是 “无人” 状态, 那么就可以使用, 使用时需要设为 “有人” 状态.
如果当前是 “有人” 状态, 那么其他人无法使用, 只能排队
一个线程上了锁,其他线程只能等待这个线程释放。
理解 “阻塞等待”.
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候, 其他线程尝试进行加锁, 就加不上了, 就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁.
注意:
上一个线程解锁之后, 下一个线程并不是立即就能获取到锁. 而是要靠操作系统来 “唤醒”. 这也就是操作系统线程调度的一部分工作.
假设有 A B C 三个线程, 线程 A 先获取到锁, 然后 B 尝试获取锁, 然后 C 再尝试获取锁, 此时 B 和 C 都在阻塞队列中排队等待. 但是当 A 释放锁之后, 虽然 B 比 C 先来的, 但是 B 不一定就能获取到锁, 而是和 C 重新竞争, 并不遵守先来后到的规则.
例:
Object lock = new Object();
// 线程t1,t2竞争lock对象里的同一把锁,t1比t2先执行,因此t2需等到t1执行完毕后才可以执行
Thread t1 = new Thread(()->{
synchronized (lock){
while(true){
try {
System.out.println("111");
// TIMED_WAITING 等待时间到了自动唤醒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"t1");
t1.start();
Thread t2 = new Thread(()->{
synchronized (lock){
System.out.println("happy day");
}
},"t2");
t2.start();
线程t1超时等待,该线程需要等待一段时间之后再恢复执行
该线程t2在等待别的线程释放资源
wait 和 notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知,但有时希望线程之间的执行有先后顺序,因此需用到以下三个方法:
- wait() / wait(long timeout): 让当前线程进入等待状态.
- notify() / notifyAll(): 唤醒在当前对象上等待的线程.
注意: wait, notify, notifyAll 都是 Object 类的方法
wait()方法
注意:wait 要搭配 synchronized 来使用,脱离 synchronized 使用 wait 会直接抛出异常.
wait 做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒, 重新尝试获取这个锁.
wait 结束等待的条件:
-
其他线程调用该对象的 notify 方法.
-
wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
-
其他线程调用该等待线程的 interrupt方法, 导致 wait 抛出 InterruptedException 异常.
例:更改t1线程,令t1线程进入等待队列,先执行t2队列
Thread t1 = new Thread(()->{ synchronized (lock){ while(true){ // 死循环 try { // 1. 线程t1,t2竞争lock对象里的同一把锁,t1比t2先执行,因此t2需等到t1执行完毕后才可以执行 // TIMED_WAITING 等待时间到了自动唤醒 // Thread.sleep(1000); // 2. 令线程进入等待状态,释放当前的锁,分配给t2继续执行 lock.wait(); System.out.println("我被唤醒了~"); } catch (InterruptedException e) { e.printStackTrace(); } } } },"t1");
yield()
yield():会使线程从运行状态转为就绪态(了解,使用不多,都不可控)
-
调用yield方法的线程会主动让出CPU资源,从运行态转为就绪态,等待被CPU继续调度。(处于就绪态的线程在等待队列中,不缺任何资源,随时可以被CPU调度)
-
但到底啥时候让出CPU,又是啥时候被CPU再次调度,都是os调度的,我们无权选择:
1) 让出之后立马又被调度了
2) 让出之后很久都不调度
Thread t1 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
// Lay线程会让出CPU,进入就绪态,等待被CPU继续调度
// 未使用yield之前,两个线程被调用的次数差不多,使用之后Z线程被调用次数变多
Thread.yield();
}
},"Lay线程");
t1.start();
Thread t2 = new Thread(() -> {
while (true) {
System.out.println(Thread.currentThread().getName());
}
},"Z线程");
t2.start();