[Java EE] 多线程 -- 初阶(1)
1.线程(Thread)
1.1 线程是什么?
- 一个线程就是一个执行流 ;
- 每个线程之间都可以按照顺序执行自己的代码 ;
- 多个线程之间 "同时"执行着多份代码
1.2 线程的作用
①并发式编程
单核 CPU 的发展遇到了瓶颈 , 要想提高算力 , 就需要多核 CPU ; 而并发式编程能更充分的利用多核 CPU 资源
有些任务场景需要 "等待IO" , 为了让等待 IO 的时间能够取做一些其他工作 , 也需要用到并发编程
② 虽然多进程也能实现 并发编程 , 但是 线程比进程更轻量
- 创建线程比创建进程更快
- 销毁线程比销毁进程更快
- 调度线程比调度进程更快
③ 线程虽然比进程轻量 , 但是还引入了 "线程池"(ThreadPool) 和 "协程"(Coroutine)
1.3 线程和进程区别
① 进程是包含线程的
- 每个进程至少包含一个线程 , 即为主线程
② 进程和进程之间不共享内存空间 ; 同一个进程的线程之间共享同一个内存空间
③ 进程是系统分配资源的最小单位 ; 线程是系统调度的最小单位
④ 一个进程挂了一般不会影响其他进程 ; 但是一个线程挂了 , 可能把同进程内的其他线程一起带走(整个线程崩溃)
1.4 Java 线程和操作系统线程的关系
线程是操作系统的概念 ; 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使用
Java 标准库中的 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
2.创建线程
方法 1 : 继承 Thread 类
class Mythread extends Thread{//run 相当于进程的入口@Overridepublic void run() {while(true){System.out.println("hello thread");//调用这个方法会抛异常,由于这个类是继承与Thread的,run方法是继承与Thread中的run//子类重写方法抛出的异常,必须是父类方法异常的子类或相同类型,且不能抛出更宽泛的异常try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo1 {public static void main(String[] args) throws InterruptedException {Thread t = new Mythread();//父类引用指向子类实例t.start();//启动线程//t.run();//只是调用这个方法,并不是创建进程while (true) {System.out.println("hello main");Thread.sleep(1000);//向上抛异常}}
}
继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.

使用 jconsole 命令来观察线程
在 Java\jdk\bin 目录下(安装 jdk 的路径) 找 jconsole.exe , 并连接




Thread.sleep(1000) 会让当前进程放弃 CPU 资源 , 进入休眠状态 (毫秒), 此时 CPU 可以调度另一个线程执行
方法 2 : 实现 Runnable 接口
class MyRunnable implements Runnable{@Overridepublic void run() {while(true){System.out.println("hello Thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}public class demo2 {public static void main(String[] args) throws InterruptedException {//创建 Thread 类实例 , 调用 Thread 的构造方法时 , 将 Runna 对象作为 target 参数Runnable myRunnable = new MyRunnable();//父类接口的引用变量指向子类实例Thread t = new Thread(myRunnable);t.start();//还是需要用到 t (Thread) 来开启线程while (true){System.out.println("hello main");Thread.sleep(1000);}}
}
实现 Runnable 接口 , this 表示的是 MyRunnable 的引用 , 需要使用 Thread.currentThread()
方法 3 : 匿名内部类创建 Thread 子类对象
public class demo3 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread() {@Overridepublic void run() {while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}};t.start();while (true){System.out.println("hello main");Thread.sleep(1000);}}
}
new Thread(){...}创建了 Thread 类的匿名内部类 , 并重写 run() 方法
方法 4 : 匿名内部类创建 Runnable 子类对象
public class demo4 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic void run() {while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}});t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
}
方法 5 : lambda 表达式创建 Runnable 子类对象
public class demo5 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(true){System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});t.start();while(true){System.out.println("hello main");Thread.sleep(1000);}}
3.Thread 类
3.1Thread 的常见构造方法
| 方法 | 说明 |
| Thread() | 创建线程对象 |
| Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
| Thread(String name) | 创建线程对象 , 并命名 |
| Thread(Runnable target , String name) | 使用 Runnable 对象创建线程对象 , 并命名 |
| [ 了解 ]Thread(ThreadGroup group , Runnable target) | 线程可以被用来分组管理 , 分好的组即为线程 |
ThreadGroup : 线程组 , 把多个线程放到一个组里 , 统一针对线程里所有的线程进行一些属性设置
class Mythread1 extends Thread{public Mythread1(String name) {super(name);}//run 相当于进程的入口@Overridepublic void run() {while(true){System.out.println("hello thread");//调用这个方法会抛异常,由于这个类是继承与Thread的,run方法是继承与Thread中的run//子类重写方法抛出的异常,必须是父类方法异常的子类或相同类型,且不能抛出更宽泛的异常try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}}
}
public class demo6 {public static void main(String[] args) {Thread t1 = new Thread(()->{while(true){System.out.println("hello t1");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}},"这是线程一");//线程命名为th1t1.start();Mythread1 t2 = new Mythread1("这是线程二");t2.start();}
}

3.2 Thread 的常见属性
| 属性 | 获取方法 |
| ID | getId() |
| 名称 | getName() |
| 状态 | getState() |
| 优先级 | getPriority() |
| 是否后台线程 | isDaemon() |
| 是否存活 | isAlive() |
| 是否被中断 | isInterrupted() |
- ID : 线程的唯一标识符 , 不同线程不会重复
- 名称 : 是用于调试工具(jstack)区分线程 , 可以通过构造方法或者 setName()自定义
- 状态 : 表示线程当前所处的状态 (新建 , 可运行 , 阻塞 , 无限期等待 , 超时等待 , 终止)
| 状态 | 含义及场景 |
| NEW(新建) | 线程已创建(如 方法,未与操作系统线程关联。 |
| RUNNABLE(可运行) | 线程调用 |
| BLOCKED(阻塞) | 线程因竞争 |
| WAITING(无限期等待) | 线程无超时地等待被其他线程唤醒,如调用 |
| TIMED_WAITING(超时等待) | 线程在指定时间内等待,超时后自动唤醒,如调用 |
| TERMINATED(终止) | 线程的 |
- 优先级 : 范围 1-10(默认 5 ) , 优先级高的线程理论上更容易被调度到
- 是否后台线程 : 后台线程(守护线程) 随 JVM 中所有非后台线程结束而终止 , 典型如垃圾回收线程
- 是否存活 : 判断 run()方法是否执行完毕 , start()后到 run()结束前返回 true
- 是否被中断 : 检测线程中断状态( 不会清楚中断标记) , 需要结合 interrupt()方法理解中断机制
部分示例 :
isAlive();
public class demo7 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{for (int i = 0; i < 3; i++) {System.out.println("hello t");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}}});System.out.println(t.isAlive());//还没有start(),一定是falset.start();while(true){System.out.println(t.isAlive());//在run()结束之前,一定是trueThread.sleep(1000);}}
}

设置后台线程 setDaemon();
public class demo8 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while (true) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}});t.setDaemon(true);//设置t为后台线程,会随着非后台线程的结束而终止,也就是说main线程结束t线程也结束//如果不加该语句,则main线程结束t线程继续执行,互不干扰t.start();for (int i = 0; i < 3; i++) {System.out.println("hello main");Thread.sleep(1000);}System.out.println("main 线程结束");}
}

自己编写的线程一般默认为前台线程
3.3 启动线程 -- start()
run()是线程的入口 , 不需要手动调用 , start()是调用系统 api
调用 start()方法 , 才真的是在操作系统的底层创建了一个线程
public class demo11 {public static void main(String[] args) {Thread t = new Thread(()->System.out.println("线程1"));//t.start();//t.run();t.start();}
}
每个 Thread 对象 , 只能 start 一次 , 否则抛异常

创建 t 线程的逻辑在 main 中 , 因此一定是先执行 main 线程
3.4 中断线程
让线程的入口方法 , 尽快结束
方法 1 : 引入一个isFinished 变量(引入自定义的变量来作为标志位)
public class Demo10 {private static boolean isFinished = false;public static void main(String[] args) throws InterruptedException {//boolean isFinished = false;//如果将变量放在这里,会触发lambda变量捕获//此时如果main线程执行完了,对应的isFinished就销毁了//从而改成内部类访问外部类成员Thread t = new Thread(() -> {//内部类访问外部类成员while (!isFinished) {System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("thread 结束");});t.start();Thread.sleep(3000);isFinished = true;}
}

方法 2 : 方法 2 : 使⽤ Thread.interrupted() 或者Thread.currentThread().isInterrupted() 代替⾃定义标志位
Thread.currentThread() : 用来返回线程的的名称
| 方法 | 说明 |
| public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知,否则设置标识位 |
| public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清楚标志位 |
| public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,标志后不清楚标志位 |
public class demo9 {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(()->{while(!Thread.currentThread().isInterrupted()){//判定线程是否被终止了System.out.println("hello thread");try {Thread.sleep(1000);} catch (InterruptedException e) {//throw new RuntimeException(e)break;}}System.out.println("t线程结束");});t.start();Thread.sleep(3000);System.out.println("main 线程尝试终止 t 线程");t.interrupt();//主动去终止线程,由于线程中大部分时间都在休眠,此时还会唤醒sleep这样的阻塞方法,捕获到异常从而终止,针对异常的处理,使用break结束循环//会改变isInterrupted()的值为true}
}
使用 t.interrupted()方法通知线程结束
thread 收到通知的⽅式有两种:
- 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通
知,清除中断标志
当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽
略这个异常, 也可以跳出循环结束线程. - 否则,只是内部的⼀个中断标志被设置,thread 可以通过
Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到
异常处理

如果异常不做处理

此处认为这个线程可以是立即结束(break),等会结束(在 catch 中的 break 前编写需要的代码),还是不结束(在 catch 中不写 break 也就是忽略这个终止信号)
3.5 等待一个线程 -- join()
| 方法 | 说明 |
| public void join() | 等待线程结束 |
| public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
| public void join(long millis,int nanos) | 同理,但可以更高精度 |
public class demo12 {public static void main(String[] args) throws InterruptedException {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();}}System.out.println("thread 线程结束");});System.out.println("hello main");t.start();System.out.println("main 线程");t.join(3000);//main线程最多等3秒System.out.println("main 线程结束");}
}
在 main 线程中调用 , 意味着让 main 线程等待 t 线程执行完毕在接着执行 main

3.6 获取当前线程的引用(前面使用过)
| 方法 | 说明 |
| public static Thread currentThread(); | 返回当前线程对象的引用 |
哪个线程调用这个方法 , 返回哪个线程的引用
3.7 休眠当前线程(前面使用过)
| 方法 | 说明 |
| public static void sleep(long millis)throws InterruptedException | 休眠当前线程 millis 毫秒 |
| public static void sleep(long millis,int nanos)throws InterruptedException | 同理,获取更高精度 |
由于线程的调度不可控 , 所以这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间
sleep(0)特殊写法,让当前线程放弃 CPU 资源 , 等待操作系统重新调度
