- 认识多线程
- 掌握多线程程序的编写
- 掌握多线程的状态
一. 认识线程(Thread)
1概念
1) 线程是什么
⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏着多份代码.
还是回到我们之前的银⾏的例⼦中。之前我们主要描述的是个⼈业务,即⼀个⼈完全处理⾃⼰的业务。我们进⼀步设想如下场景:
⼀家公司要去银⾏办理业务,既要进⾏财务转账,⼜要进⾏福利发放,还得进⾏缴社保。如果只有张三⼀个会计就会忙不过来,耗费的时间特别⻓。为了让业务更快的办理好,张三⼜找来两位同事李四、王五⼀起来帮助他,三个⼈分别负责⼀个事情,分别申请⼀个号码进⾏排队,⾃此就有了三个执⾏流共同完成任务,但本质上他们都是为了办理⼀家公司的业务。此时,我们就把这种情况称为多线程,将⼀个⼤任务分解成不同⼩任务,交给不同执⾏流就分别排队 执⾏。其中李四、王五都是张三叫来的,所以张三⼀般被称为主线程(Main Thread)。
2) 为啥要有线程
⾸先, "并发编程" 成为 "刚需".
- 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU资源.
- 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.(线程就是轻量级进程)
- 创建线程⽐创建进程更快.
- 销毁线程⽐销毁进程更快.
- 调度线程⽐调度进程更快.
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池"(ThreadPool) 和 "协程"(Coroutine)
3) 进程和线程的区别
- 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
- 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
⽐如之前的多进程例⼦中,每个客⼾来银⾏办理各⾃的业务,但他们之间的票据肯定是不想让别⼈知道的,否则钱不就被其他⼈取⾛了么。⽽上⾯我们的公司业务中,张三、李四、王五虽然是不同的执⾏流,但因为办理的都是⼀家公司的业务,所以票据是共享着的。这个就是多线程和多进程的最⼤区别。
- 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
- ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
4) Java 的线程 和 操作系统线程 的关系
api:application programming interface(应用程序编程接口)
- 操作系统提供的原生api是c写的
- 不同操作系统的线程api不相同
2 第⼀个多线程程序
- 每个线程都是⼀个独⽴的执⾏流
- 多个线程之间是 "并发" 执⾏的.
3 创建线程
⽅法1 继承 Thread 类
class MyThread extends Thread{
@Override
//run相当于线程的入口函数
public void run() {
System.out.println("hello world");
}
}
(2)创建 MyThread 类的实例
Thread t=new MyThread();
(3)调⽤ start ⽅法启动线程
//真正在系统中创建出一个线程
t.start();
(4)休眠
try {
Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
(5)run
在执行线程时,不需要显示运行run方法,JVM自动调用
Tip:
在自己写的MyThread类里面不允许用throws,只能try-catch,因为其父类Thread里面没有实现该功能,但main函数中可以
实际开发中,异常的处理方式
1.记录异常信息作为日志.后续程序员根据日志调查问题
程序仍然正常往后执行逻辑,不会因为这个异常就终止(对于服务器非常关键的)2.进行重试,有的异常是概率性的(网络通讯)
3.特别严重的问题,必须立即马上处理的问题
通过短信/邮件/微信/电话 通知程序员 (报警机制)
服务器和客户端指的是两个程序
服务器(server):被动接受请求,返回响应的一方
客户端(client):主动发起请求的一方
- 客户端给服务器发送的数据,叫做“request"请求
- 服务器给客户端返回的叫做“response”响应
- 通常一个服务器可以给多个客户端提供服务
- 服务器基本7*24待命
根据输出可以知道:多个线程的调度是随机的(“抢占式执行”)
Q:可以控制输出顺序吗?
A:输出顺序是操作系统内核的调度器控制的,没法在应用应用程序中编写代码控制 (调度器没有提供 api 的)
唯一能做的就是给线程设置优先级(但是优先级,对于操作系统来说,也是仅供参考,不会严格的定量的遵守)
如果直接使用run方法,而没有start,那么MyTread实质上没有创建出进程,只有main进程,遇到run中的死循环之后无法退出。
package Thread;
class MyThread extends Thread{
@Override
//run相当于线程的入口函数
public void run() {
while(true){
System.out.println("hello run");
try {
Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
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);
}
}
}
⽅法2 实现 Runnable 接⼝
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
2. 创建 Thread 类实例, 调⽤ Thread 的构造⽅法时将 Runnable 对象作为 target 参数.
Runnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
3. 调⽤ start ⽅法
t.start();
总代码:
package Thread;
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class demo2 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable=new MyRunnable();
Thread t=new Thread(runnable);
t.start();
while (true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
Runnable最终还是要通过 Thread,真正创建线程
线程里要干啥, 通过 Runnable 来表示(而不是通过直接重写 Thread的run 来表示了)
Q:如何判断是用哪种?
A:根据线程要执行的任务的定义,是放到 Thread 里面,还是放到外面(Runnable 中)
Q:使用Runnable有什么好处吗?
A:解耦合。要执行的任务本身,和 线程这个概念,能够解耦合,从而后续如果变更代码.(比如不通过线程执行这个任务,通过其他方式.….)
采用 Runnable 这样的方案,代码的修改就会更简单.
对⽐上⾯两种⽅法:
- 继承 Thread 类, 直接使⽤ this 就表⽰当前线程对象的引⽤.
- 实现 Runnable 接⼝, this 表⽰的是 MyRunnable 的引⽤. 需要使⽤Thread.currentThread()
⽅法3 实现Tread的匿名内部类
package Thread;
public class demo3 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(){
while(true){
System.out.println("hello run");
try {
Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
thread.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
⽅法4 匿名内部类创建 Runnable ⼦类对象
package Thread;
public class demo4 {
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("hello run");
try {
Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
Thread thread=new Thread(runnable);
thread.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
⽅法5 lambda 表达式创建 Runnable ⼦类对象
package Thread;
public class demo5 {
public static void main(String[] args) throws InterruptedException {
Thread thread=new Thread(()->{
while (true) {
System.out.println("hello run");
try {
Thread.sleep(1000);//sleep是静态方法,表示休眠,休息一会再执行,用ms为单位
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
while(true){
System.out.println("hello main");
Thread.sleep(1000);
}
}
}
4 多线程的优势-增加运⾏速度
- 使⽤ System.nanoTime() 可以记录当前系统的 纳秒 级时间戳.
- serial 串⾏的完成⼀系列运算.
- concurrency 使⽤两个线程并⾏的完成同样的运算.
二. Thread 类及常⻅⽅法
Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关联。
⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。
1 Thread 的常⻅构造⽅法
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");
main方法结束了,主线程就结束了
以前认知里main方法结束,程序就执行完毕是针对单线程程序的
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());
}
}
3 启动⼀个线程 - start()
之前我们已经看到了如何通过覆写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线程就开始运⾏了。
- 覆写 run ⽅法是提供给线程要做的事情的指令清单
- 线程对象可以认为是把 李四、王五叫过来了
- ⽽调⽤ start() ⽅法,就是喊⼀声:”⾏动起来!“,线程才真正独⽴去执⾏了。
调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程
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 收到通知的⽅式有两种:
1. 如果线程因为调⽤ wait/join/sleep 等⽅法⽽阻塞挂起,则以 InterruptedException 异常的形式通 知,清除中断标志
◦ 当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择忽略这个异常, 也可以跳出循环结束线程.
◦ Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置,不清除中断标志
2. 否则,只是内部的⼀个中断标志被设置,thread 可以通过这种⽅式通知收到的更及时,即使线程正在 sleep 也可以⻢上收到。
5 等待⼀个线程 - join()
有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四转账成功,才决定是否存钱,这时我们需要⼀个⽅法明确等待线程的结束。
package Thread;
public class ThreadDemo1 {
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 注释掉,现象会是怎么样的呢?
6 获取当前线程引⽤
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
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());
}
}
三、 线程的状态
1 观察线程的所有状态
Thread.State
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
- NEW: 安排了⼯作, 还未开始⾏动
- RUNNABLE: 可⼯作的. ⼜可以分成正在⼯作中和即将开始⼯作.
- BLOCKED: 这⼏个都表⽰排队等着其他事情
- WAITING: 这⼏个都表⽰排队等着其他事情
- TIMED_WAITING: 这⼏个都表⽰排队等着其他事情
- TERMINATED: ⼯作完成了.
2. 线程状态和状态转移的意义
⼤家不要被这个状态转移图吓到,我们重点是要理解状态的意义以及各个状态的具体意思。
还是我们之前的例⼦:
刚把李四、王五找来,还是给他们在安排任务,没让他们⾏动起来,就是 NEW 状态;
当李四、王五开始去窗⼝排队,等待服务,就进⼊到 RUNNABLE 状态。该状态并不表⽰已经被银⾏⼯作⼈员开始接待,排在队伍中也是属于该状态,即可被服务的状态,是否开始服务,则看调度器的调度;
当李四、王五因为⼀些事情需要去忙,例如需要填写信息、回家取证件、发呆⼀会等等时,进⼊BLOCKED 、 WATING 、TIMED_WAITING 状态,⾄于这些状态的细分,我们以后再详解;
如果李四、王五已经忙完,为 TERMINATED 状态。
所以,之前我们学过的 isAlive() ⽅法,可以认为是处于不是 NEW 和 TERMINATED 的状态都是活着的。
3 观察线程的状态和转移
package Thread;
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
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 线程在⽆限等待唤醒