1.创建线程的三种方式
1. 继承Thread类并重写run方法。
2. 实现Runnable接口,然后实现其run方法。
3. 通过Callable和Future创建线程。
将我们希望线程执行的代码放到 run 方法中,然后通过 start 方法来启动线程,start 方法首先为线程的执行准备 好系统资源,然后再去调用run方法。
1.1 继承Thread类并重写run方法
- 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体(线程体)。
- 创建Thread子类的实例,即创建了线程对象。
- 调用线程对象的start()方法来启动该线程。
- Java不允许多继承。
/**
* @ClassName MyThread
* @author: shouanzh
* @Description 继承Thread类创建线程类 (extends)
* @date 2022/3/2 20:26
*/
@Slf4j
public class MyThread extends Thread{
// 通过构造函数传递参数
private String name;
public MyThread(String name) {
this.name = name;
}
@Override
public void run() {
// 具体任务逻辑
System.out.println("继承Thread类创建线程类...");
log.info("继承Thread类创建线程类...");
}
}
/**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) {
MyThread myThread = new MyThread("T1");
myThread.setName("T1");
MyThread myThread2 = new MyThread("T2");
myThread.setName("T2");
// 启动线程
myThread.start();
myThread2.start();
}
}
1.2 使用Runnable配合Thread
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
- 调用线程对象的start()方法来启动该线程。
- Runnable接口支持多继承
/**
* @ClassName RunnableThreadTest
* @author: shouanzh
* @Description 使用Runnable配合Thread
* @date 2022/3/2 20:38
*/
@Slf4j
public class RunnableThreadTest implements Runnable{
// 通过变量和方法传递数据
private String name;
public void setName(String name) {
this.name = name;
}
@Override
public void run() {
// 创建线程任务
System.out.println("继承Thread类创建线程类...");
log.info("继承Thread类创建线程类...");
}
}
/**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) {
RunnableThreadTest runnableThread = new RunnableThreadTest();
// Runnable可以实现多个相同的程序代码的线程去共享同一个资源
Thread thread = new Thread(runnableThread);
Thread thread2 = new Thread(runnableThread);
thread.start();
thread2.start();
}
}
1.3 通过Callable和FutureTask创建线程
- 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
- 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
- 使用FutureTask对象作为Thread对象的target创建并启动新线程。
- 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
/**
* @ClassName CallableThreadTest
* @author: shouanzh
* @Description 通过Callable和FutureTask创建线程
* @date 2022/3/2 20:46
*/
public class CallableThreadTest implements Callable<String> {
@Override
public String call() throws Exception {
return "hello word";
}
}
**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableThreadTest callableThread = new CallableThreadTest();
FutureTask<String> futureTask = new FutureTask<>(callableThread);
Thread thread = new Thread(futureTask,"T1");
thread.start();
// 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
String result = futureTask.get();
System.out.println(result);
}
}
2.Runnable和Thread的区别
Runnable与Thread,前者的实现方式是实现其接口即可,后者的实现方式是继承其类。两者实现方式带来最明显的区别就是,由于Java不允许多继承,因此实现了Runnable接口可以再继承其他类,但是Thread明显不可以。
Runnable可以实现多个相同的程序代码的线程去共享同一个资源,而Thread不可以?
以经典的卖票案列:总共只有5张动车票了,有2个窗口在卖。
1. Runnable方式:
/**
* @ClassName RunnableThreadTest
* @author: shouanzh
* @Description 使用Runnable配合Thread
* @date 2022/3/2 20:38
*/
@Slf4j
public class RunnableThreadTest implements Runnable {
private int ticket = 5;
@Override
public void run() {
while (true) {
System.out.println("Runnable ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
/**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
RunnableThreadTest runnableThread = new RunnableThreadTest();
// Runnable可以实现多个相同的程序代码的线程去共享同一个资源
Thread thread = new Thread(runnableThread);
Thread thread2 = new Thread(runnableThread);
thread.start();
thread2.start();
}
}
运行结果:
Runnable ticket = 5
Runnable ticket = 3
Runnable ticket = 2
Runnable ticket = 1
Runnable ticket = 0
Runnable ticket = 4
Process finished with exit code 0
一个任务可以启动多个线程,通过Runnable方式实现的线程,实际是开辟一个线程,将任务传递进去,由此线程执行。可以实例化多个 Thread对象,将同一任务传递进去,也就是一个任务可以启动多个线程来执行它。这些线程执行的是同一个任务,所以他们的资源是共享。
2. Thread方式:
**
* @ClassName MyThread
* @author: shouanzh
* @Description 继承Thread类创建线程类 (extends)
* @date 2022/3/2 20:26
*/
@Slf4j
public class MyThread extends Thread{
private int ticket = 5;
@Override
public void run() {
while (true) {
System.out.println("Runnable ticket = " + ticket--);
if (ticket < 0) {
break;
}
}
}
}
测试:
方式一:
因为一个线程只能启动一次,通过Thread实现线程时,线程和线程所要执行的任务是捆绑在一起的。也就使得一个任务只能启动一个线程,不同的线程执行的任务是不相同的,所以没有必要,也不能让两个线程共享彼此任务中的资源。
/**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 每个线程都独立,不共享资源,每个线程都卖出了5张票,总共卖出了10张。
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
}
}
运行结果
Runnable ticket = 5
Runnable ticket = 4
Runnable ticket = 3
Runnable ticket = 2
Runnable ticket = 1
Runnable ticket = 0
Runnable ticket = 5
Runnable ticket = 4
Runnable ticket = 3
Runnable ticket = 2
Runnable ticket = 1
Runnable ticket = 0
Process finished with exit code 0
方式二:
/**
* @ClassName CreateThread
* @author: shouanzh
* @Description 测试
* @date 2022/3/2 20:29
*/
public class CreateThread {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread myThread = new MyThread();
Thread myThread2 = new Thread(myThread);
Thread myThread1 = new Thread(myThread);
myThread1.start();
myThread2.start();
}
}
运行结果
Runnable ticket = 5
Runnable ticket = 3
Runnable ticket = 2
Runnable ticket = 1
Runnable ticket = 0
Runnable ticket = 4
Process finished with exit code 0
其中方式一创建了两个MyThread对象,每个对象都有自己的ticket成员变量,当然会多卖。
方式二只创建了一个Thread对象,效果和Runnable一样。
实现Runable只是方便资源共享。当然继承Thrad也可以资源共享。
3.Thread类源代码剖析
-
Thread 类也实现了 Runnable接口,因此实现了Runnable 接口中的run方法。
-
构造方法
-
当使用第一种方式来生成线程对象时,我们需要重写 run 方法,因为 Thread 类的run 方法此时什么事情也不做。
-
当使用第二种方式来生成线程对象时,我们需要实现 Runnable 接口的run 方法,然后使用 new Thread(new MyThread()) MyThread 已经实现了Runnable接口,来生成线程对象,这时的线程对象的run方法 会调用 MyThread 类的run方法,这样我们自己编写的run 方法就执行了。
4.线程运行原理
4.1 栈与栈帧
我们都知道JVM中由堆,栈,方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动 后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的
栈帧debug
/**
* @ClassName TestFrame
* @author: shouanzh
* @Description TestFrame
* @date 2022/3/2 23:28
*/
public class TestFrame {
public static void main(String[] args) {
method1(10);
}
public static void method1(int x) {
int y = x + 1;
Object object = method2();
System.out.println(object);
}
public static Object method2() {
Object object = new Object();
return object;
}
}
method2()执行完出栈
method1()执行完出栈
结束整个代码的运行
4.2 线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程
- 线程的 cpu 时间片用完
- 垃圾回收(STW)
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当Thread Context Switch发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 线程的状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
5.Thread的常见方法
5.1 调用start 与 run方法的区别
- 直接调用 run() 是在主线程中执行了 run(),没有启动新的线程
- 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法中的代码
@Slf4j
public class CreateThread {
public static void main(String[] args) {
Thread thread = new Thread("T1"){
@Override
public void run() {
log.info("T1...");
}
};
// 使用 start() 是启动新的线程,通过新的线程间接执行 run()方法中的代码
thread.start(); // 2022-03-03 20:49:49 [T1] - T1...
// 直接调用 run() 是在主线程中执行了 run(),没有启动新的线程
thread.run(); // 2022-03-03 20:49:19 [main] - T1...
}
}
5.2 sleep 与 yield
sleep的状态
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("T1"){
@SneakyThrows
@Override
public void run() {
Thread.sleep(2000);
log.info("T1...");
}
};
thread.start();
// T1的线程先执行了,这时T1的状态为RUNNABLE
log.info("T1的State:{}",thread.getState()); // T1的State:RUNNABLE
// 主线程休眠
Thread.sleep(500);
log.info("T1的State:{}",thread.getState()); // T1的State:TIMED_WAITING
}
}
sleep的打断
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("T1"){
@Override
public void run() {
try {
log.info("T1 enter sleep...");
Thread.sleep(2000);
} catch (InterruptedException e) {
log.info("T1 wake up ...");
e.printStackTrace();
}
}
};
thread.start();
Thread.sleep(1000);
log.info("interrupt...");
thread.interrupt();
}
}
TimeUnit
TimeUnit.SECONDS.sleep(1);
yield使cpu调用其它线程,但是cpu可能会再分配时间片给该线程;
而sleep需要等过了休眠时间之后才有可能被分配cpu时间片
5.3 线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它,
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
thread.setPriority(Thread.MAX_PRIORITY);
5.4 join方法详解
等待调用join() 的线程结束。
@Slf4j
public class CreateThread {
static int r = 0;
public static void main(String[] args) {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join();
// 这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
// 如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
log.debug("结果为:{}", r);
log.debug("结束");
}
}
如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
图解
下图, 因为开辟了t1线程. 此时程序中有两个线程; main线程和t1线程; 此时在main线程中调用t1.join, 所以main线程只能阻塞等待t1线程执行完. t1线程在1s后将r=10, t1线程执行完, 此时main线程才会接着执行
有时效的join(long n)
@Slf4j
public class CreateThread {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
},"t1");
t1.start();
t1.join(1000);
// 这里如果不加t1.join(), 此时主线程不会等待t1线程给r赋值, 主线程直接就输出r=0结束了
// 如果加上t1.join(), 此时主线程会等待到t1线程执行完才会继续执行.(同步), 此时r=10;
log.debug("结果为:{}", r);
log.debug("结束");
}
}
5.5 interrupt 方法详解
该方法用于打断 sleep,wait,join的线程, 在阻塞期间cpu不会分配给时间片
- 如果是打断因sleep、 wait 、join方法而被阻塞的线程,会将打断标记置为false
- 如果一个线程在在运行中被打断,打断标记会被置为true
打断sleep的线程,会清空打断状态,以 sleep为例
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
log.info("sleep...");
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(500);
log.debug("111是否被打断?{}",t1.isInterrupted());
t1.interrupt();
log.debug("222是否被打断?{}",t1.isInterrupted());
Thread.sleep(500);
log.debug("222是否被打断?{}",t1.isInterrupted());
log.debug("主线程");
}
}
打断正常运行的线程
如果一个线程在在运行中被打断,打断标记会被置为true
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
System.out.println("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("interrupt");
t1.interrupt();
System.out.println("打断标记为: "+t1.isInterrupted());
}
}
5.6 设计模式-两阶段终止模式
代码举例:
public class Test7 {
public static void main(String[] args) throws InterruptedException {
Monitor monitor = new Monitor();
monitor.start();
Thread.sleep(3500);
monitor.stop();
}
}
class Monitor {
Thread monitor;
/**
* 启动监控器线程
*/
public void start() {
//设置线控器线程,用于监控线程状态
monitor = new Thread() {
@Override
public void run() {
//开始不停的监控
while (true) {
//判断当前线程是否被打断了
if(Thread.currentThread().isInterrupted()) {
System.out.println("处理后续任务");
//终止线程执行
break;
}
System.out.println("监控器运行中...");
try {
//线程休眠
Thread.sleep(1000); // 情况1 阻塞打断
System.out.println("记录日志"); // 情况二:正常运行线程被打断
} catch (InterruptedException e) {
e.printStackTrace();
//如果是在休眠的时候被打断,不会将打断标记设置为true,这时要重新设置打断标记
Thread.currentThread().interrupt();
}
}
}
};
monitor.start();
}
/**
* 用于停止监控器线程
*/
public void stop() {
//打断线程
monitor.interrupt();
}
}
5.7 interrupt 打断 park
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.info("park");
LockSupport.park();
log.info("uppark");
log.info("打断状态{}",Thread.currentThread().isInterrupted());
// log.info("打断状态{}",Thread.interrupted());
// LockSupport.park(); // 打断状态为 true 时 无效 Thread.interrupted() 清除打断标记
log.info("uppark");
}, "t1");
t1.start();
Thread.sleep(1000);
System.out.println("interrupt");
t1.interrupt();
}
}
5.8 不推荐使用的方法
5.9 主线程与守护线程
默认情况下, Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其 它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
代码举例
@Slf4j
public class CreateThread {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
// 判断当前线程是否被打断了
if (Thread.currentThread().isInterrupted()) {
System.out.println("处理后续任务");
//终止线程执行
break;
}
}
log.info("结束");
}, "Daemon");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.info("结束");
}
}
- 垃圾回收器线程就是一种守护线程
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
5.10 sleep、yield、wait、join 对比
- sleep,join,yield,interrupted是Thread类中的方法
- wait/notify是object中的方法
- sleep 不释放锁、释放cpu
- join 释放锁、抢占cpu
- yiled 不释放锁、释放cpu
- wait 释放锁、释放cpu
6.线程状态
6.1 操作系统-五种状态
从操作系统方面描述
6.2 线程-六种状态
这是从 Java API 层面来描述的
根据Thread.State 枚举,分为六种状态
代码举例:
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) {
// new 状态 没有调用start()
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
// runnable 状态
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) {
}
}
};
t2.start();
// TERMINATED
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
// timed_waiting 显示阻塞状态
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
// waiting 状态
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
// 等待t2结束。一直等待
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
// blocked 状态
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
}
}
7.应用之统筹规划
代码实现
/**
* @ClassName Test5
* @author: shouanzh
* @Description 应用之统筹
* @date 2022/3/4 21:50
*/
@Slf4j(topic = "c.Test")
public class Test5 {
public static void main(String[] args) {
// 洗水壶 烧水 串行
Thread t1 = new Thread(() -> {
log.debug("洗水壶");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("烧水");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"王昊");
// 洗茶壶 洗茶杯 拿茶叶
Thread t2 = new Thread(() -> {
log.debug("洗茶壶");
log.debug("洗茶杯");
log.debug("拿茶叶");
try {
// 等待t1结束
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// t1结束
log.debug("泡茶");
},"小赵");
t1.start();
t2.start();
}
}