目录
1. 认识线程(Thread)
1) 线程是什么
2) 为啥要有线程
3) 进程和线程的区别
2.第⼀个多线程程序
3.多线程的其他创建方式
方法二:实现 Runnable 接⼝
方法三:匿名内部类
方法四:实现Runable, 重写run, 匿名内部类
方法五:使用lambda表达式 (常用到的写法)
2. Thread 类及常⻅⽅法
2.1 Thread 的常⻅构造⽅法
2.2 Thread 的⼏个常⻅属性
关于前台进程和后台进程:
使用 setDaemon(true) 可以将进程设为后台进程
isAlive()的作用
2.3 启动⼀个线程 - start()
面试题: start 和 run 的区别?
2.4 中断⼀个线程
2.5 等待⼀个线程 - join()
2.6 获取当前线程引⽤
2.7 休眠当前线程
2.8 多线程的优势-增加运⾏速度
3. 线程的状态
3.1 观察线程的所有状态
1. 认识线程(Thread)
1) 线程是什么
⼀个线程就是⼀个 "执⾏流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏ 着多份代码.
2) 为啥要有线程
⾸先, "并发编程" 成为 "刚需".
• 单核 CPU 的发展遇到了瓶颈. 要想提⾼算⼒, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU 资源.
• 有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.
• 创建线程⽐创建进程更快.
• 销毁线程⽐销毁进程更快.
• 调度线程⽐调度进程更快.
3) 进程和线程的区别
• 进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
• 进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间.
• 进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
• ⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整 个进程崩溃).
2.第⼀个多线程程序
感受多线程程序和普通程序的区别:
• 每个线程都是⼀个独⽴的执⾏流
• 多个线程之间是 "并发" 执⾏的.
package thread;
import static java.lang.Thread.sleep;
class MyThread extends Thread {
@Override
public void run() {
// run 方法就是该线程的入口方法
while(true) {
System.out.println("hello thread");
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
//根据上面的类,创建出实例
Thread t = new MyThread();
// 调用 Thread 的start方法,才会真正调用系统 api , 在系统内核中创建出线程
t.start();
while(true) {
System.out.println("hello main");
sleep(1000);
}
}
}
在上面的代码中:
run 方法是线程的入口,每个线程跑起来,都会执行一些逻辑.
运行程序后,可以看出两个线程都在并发执行, t 线程打印 "hello tread" 语句, main 主线程打印 "hello main" 语句.
为什么"hello main" 会被先打印出来:
3.多线程的其他创建方式
方法二:实现 Runnable 接⼝
package thread;
class MyThread3 implements Runnable {
@Override
public void run() {
while(true) {
System.out.println("hello runable");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadDemo3 {
public static void main(String[] args) {
Runnable runnable = new MyThread3();
Thread t = new Thread(runnable);
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
方法三:匿名内部类
package thread;
public class ThreadDemo4 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
while (true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while (true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
方法四:实现Runable, 重写run, 匿名内部类
package thread;
public class ThreadDemo5 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello runable");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();
while(true) {
System.out.println("hello main");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
方法五:使用lambda表达式 (常用到的写法)
这中写法比较简洁:
package thread;
public class ThreadDemo6 {
public static void main(String[] args) {
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");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2. Thread 类及常⻅⽅法
Thread 类是 JVM ⽤来管理线程的⼀个类,换句话说,每个线程都有⼀个唯⼀的 Thread 对象与之关 联。 ⽤我们上⾯的例⼦来看,每个执⾏流,也需要有⼀个对象来描述,类似下图所⽰,⽽ Thread 类的对象 就是⽤来描述⼀个线程执⾏流的,JVM 会将这些 Thread 对象组织起来,⽤于线程调度,线程管理。
2.1 Thread 的常⻅构造⽅法
对于其中的两个方法:
实例:
package thread;
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"这是我的线程");
t.start();
}
}
运行后, 使用jconsole来进行监视, 就可以更方便的查看我们运行的线程了:
2.2 Thread 的⼏个常⻅属性
• ID 是线程的唯⼀标识,不同线程不会重
• 名称是各种调试⼯具⽤到
• 状态表⽰线程当前所处的⼀个情况,下⾯我们会进⼀步说明
• 优先级⾼的线程理论上来说更容易被调度到
• 关于后台线程,需要记住⼀点:JVM会在⼀个进程的所有⾮后台线程结束后,才会结束运⾏。
• 是否存活,即简单的理解,为 run ⽅法是否运⾏结束了
• 线程的中断问题,下⾯我们进⼀步说明
关于前台进程和后台进程:
我们代码创建的线程,默认就是前台线程,会阻止进程结束, 只要前台线程没有执行完, 进程就不会结束. 即使main已经执行完毕了.
package thread;
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"这是我的线程");
t.start();
System.out.println("main 执行完毕");
}
}
使用 setDaemon(true) 可以将进程设为后台进程
设为 true 是后台进程, 后台不会阻止进程结束
不设为 true 是前台 , 前台会阻止进程结束
package thread;
public class ThreadDemo7 {
public static void main(String[] args) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
while(true) {
System.out.println("hello thread");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},"这是我的线程");
//设置为后台进程
t.setDaemon(true);
t.start();
System.out.println("main 执行完毕");
}
}
isAlive()的作用
isAlive() 表示了内核中的线程 (pcb) 是否还存在, java代码中定义的线程对象 (Thread) 实例, 虽然表示一个线程, 这个对象本身的生命周期, 和内核中的pcb生命周期是不完全一样的.
package thread;
public class ThreadDemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()-> {
// 这个线程的运行时间大概是1s
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("start 之前: " + t.isAlive());
t.start();
System.out.println("start 之后: " + t.isAlive());
Thread.sleep(2000);
// 2s 之后, 线程 t 已经结束了
System.out.println("t 结束后: " + t.isAlive());
}
}
2.3 启动⼀个线程 - start()
之前我们已经看到了如何通过覆写 run ⽅法创建⼀个线程对象,但线程对象被创建出来并不意味着线 程就开始运⾏了。
调⽤ start ⽅法, 才真的在操作系统的底层创建出⼀个线程.
调用start 创建出新的线程, 本质上是 start 会调用系统的 api , 来完成创建线程的操作.
面试题: start 和 run 的区别?
2.4 中断⼀个线程
如何中断一个线程呢?⽬前常⻅的有以下两种⽅式:
1. 通过共享的标记来进⾏沟通
2. 调⽤ interrupt() ⽅法来通知
⽰例-1: 使⽤⾃定义的变量来作为标志位.
public class ThreadDemo12 {
private static boolean isQuit = false;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!isQuit) {
System.out.println("我是一个线程,工作中!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程工作完毕");
});
t.start();
Thread.sleep(3000);
System.out.println("让 t 线程结束");
isQuit = true;
}
}
注意: isQuit 不能是局部变量, 这里的变量如果是局部变量,必须是 final 或 "事实final"修饰, 由于此处的isQuit 要被修改, 不能写成 final 或 "事实final", 所以只能写成成员变量. 为啥写作成员变量就可以了呢? 因为lambda表达式本质是"函数式接口" ->匿名内部类, 内部类访问外部类的成员, 这是可以的.
⽰例-2: 使⽤ Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替⾃定义标志位.
public class ThreadDemo13 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(!Thread.currentThread().isInterrupted()) {
System.out.println("我是一个线程,工作中!");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
// 加上 break ,此时抛出异常后,线程也会结束
break;
}
}
System.out.println("线程执行完毕");
});
t.start();
Thread.sleep(3000);
System.out.println("让t线程结束");
//使用 interrupt 方法,来修改上面"Thread.currentThread().isInterrupted()"的值
t.interrupt();
}
}
2.5 等待⼀个线程 - join()
有时,我们需要等待⼀个线程完成它的⼯作后,才能进⾏⾃⼰的下⼀步⼯作。例如,张三只有等李四 转账成功,才决定是否存钱,这时我们需要⼀个⽅法明确等待线程的结束。
public class ThreadDemo14 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("我是一个线程,正在工作中...");
try {
Thread.sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程执行结束");
});
t.start();
t.join();
System.out.println("这是主线程,期望这个日志在 t 结束后打印");
}
}
在 main 线程中调用 t.join, 可以让 main 线程等待 t 线程结束.
执行 join 的时候, 就会看 t 线程是否在运行, 如果 t 运行中, main 线程就会阻塞(main 线程就暂时不去参与 cpu 执行了)
如果 t 运行结束, main 线程就会从阻塞中恢复过来, 并且继续往下执行.
任何一个线程都可以调用 join , 哪个线程调用 join , 那个线程就阻塞等待.
join的其他构造方法:
2.6 获取当前线程引⽤
如果是继承 Thread, 直接使用 this 拿到线程(Thread)的引用
package thread;
class MyThread extends Thread {
@Override
public void run() {
// 这个代码中,如果想要获取到线程的引用,直接使用 this 即可
System.out.println(this.getId() + ", " + this.getName());
}
}
public class ThreadDemo16 {
public static void main(String[] args) {
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
}
}
如果是 Runnable 或者 lambda 的方式, this 就无能为力了, 此时 this 已经不再指向 Thread 对象了.
就只能使用 Thread.currentThread() 了
package thread;
public class ThreadDemo17 {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
Thread t = Thread.currentThread();
System.out.println(t.getName());
});
Thread t2 = new Thread(() ->{
Thread t = Thread.currentThread();
System.out.println(t.getName());
});
t1.start();
t2.start();
}
}
2.7 休眠当前线程
也是我们⽐较熟悉⼀组⽅法,有⼀点要记得,因为线程的调度是不可控的,所以,这个⽅法只能保证 实际休眠时间是⼤于等于参数设置的休眠时间的。
2.8 多线程的优势-增加运⾏速度
下面,我们将通过完成 1~100亿的相加运算, 来比较单个线程和多个线程之间共同执行的速度:
首先,单个线程来完成:
两个线程来共同完成:
注意, 此处的 result 已经溢出,不考虑 result 的准确性, 只关注运行的时间.
3. 线程的状态
3.1 观察线程的所有状态
package thread;
import static java.lang.Thread.sleep;
public class ThreadDemo18 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
for(int i = 0; i < 5; i++) {
System.out.println("线程执行中...");
try {
sleep(1000);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//线程启动之前, 状态就是 NEW
System.out.println(t.getState());
t.start();
System.out.println(t.getState());
sleep(500);
System.out.println(t.getState());
t.join();
//线程运行完毕, 状态就是 TERMINATED
System.out.println(t.getState());
}
}