文章目录
什么是JUC
在 Java 5.0 提供了 java.util.concurrent ( 简称JUC ) 包, 在此包中增加了在并发编程中很常用的实用工具类, 用于定义类似于线程的自定义子系统, 包括线程池、 异步 IO 和轻量级任务框架。提供可调的、 灵活的线程池。 还提供了设计用于多线程上下文中的 Collection 实现等
- 涉及到的
包
内容java.util.concurrent
java.util.concurrent.atomic
java.util.concurrent.locks
为什么学习好多线程很重要
硬件来说
- 摩尔定律从2003年开始CPU的主频不在翻倍,而是采用多核而不是更快的主频,也就是摩尔定律的失效
- 在主频不再翻倍提高而核数不断增加的情况下,想让程序更快就必须用到并行或者并发技术
软件来说
- 面试必备
- 充分利用多核处理器
- 提高程序性能,高并发系统
- 提高程序吞吐量,异步加回调等生产需求
存在的问题
- 线程安全性问题
- 线程锁问题
- 线程性能问题
Java多线程相关概念
一把锁
- synchronized在后面讲解
两个并
并发与并行
拿餐厅吃饭举例子,餐厅来了三个客人,有这些菜的需求
- 串行:就是这个餐厅只有一个厨师,想要满足3个客人的要求,只能一道一道的去做菜,先满足完你的三道菜的需求,才能去满足我的三道菜的需求。这种按顺序一道一道满足的模式就是串行处理方式
- 并行:这个餐厅有三个厨师,每个厨师去服务自己的客户,这样就是同时满足三个客户的需求,这种处理模式就是并行处理方式
- 并发:餐厅还是只有一个厨师,但是我给其中一个客户做了一道菜,然后就转向给另一个用户做菜,按照一种时间片的概念去处理,每个时间片可能处理的事情是不一样的
如果厨师做菜的速度非常快,1分钟就能出一道菜,那么我上了一道菜,然后过了3分钟又出了一道菜,那么在现实生活中,我们就会感觉这个厨师就是专门在为我们服务,将这种思想用到计算机中,我们知道CPU的处理速度是非常快的,基本是以ns为单位的处理速度,所以在CPU在并发的执行计算机任务,在我们主观的意识中,我们就感觉这个CPU是在并行的处理任务
三个程
进程、线程、管程
进程和程序的联系
什么是程序
- 程序就是一系列有组织的文件,封装操作系统的各种API,实现不同的效果(程序就是指令序列,程序的代码放入程序段中,输出和输入的数据放入数据段中),比如QQ就是一个程序
什么是进程
- 进程就是程序执行的一次执行过程,是系统进行资源分配的一个独立最小单位(资源指CPU,内存等关键系统资源)
- 比如我登录了两次QQ,那么我在后台任务管理器,出现了两次QQ的进程,每个进程都是QQ执行的一次过程
为何引入进程
- 因为当代操作系统都是具有多道程序技术实现并发处理,在内存中会放入多个程序,那么会存在多个程序段和数据段,操作系统如何正确找到对应的程序和数据段,所以引入了进程的概念(为了使程序可以并发的执行,并且可以对并发执行的程序加以描述和控制)
进程和程序的区别
-
进程是动态的,程序是静态的
-
进程可以并发执行,但是程序不可以
-
二者无一一对应的关系
-
进程与程序又有密切的联系: 进程不能脱离具体程序而虚设, 程序规定了相应进程所要完成的动作。
-
组成不同。进程包含PCB、程序段、数据段。程序包含数据和指令代码。
-
程序是一个包含了所有指令和数据的静态实体。本身除占用磁盘的存储空间外,并不占用系统如CPU、内存等运行资源。
-
进程由程序段、数据段和PCB构成,会占用系统如CPU、内存等运行资源。
-
一个程序可以启动多个进程来共同完成。
例子
进程调度算法
- 先来先服务调度算法(FCFS)
- 短作业(进程)优先调度算法
- 高优先权优先调度算法 1非抢占式优先权算法 2抢占式优先权调度算法 3高响应比优先调度算法
- 高响应调度算法的优先级的计算
线程和进程的联系
什么是线程
- 现代操作系统都是多线程操作系统,线程就是进程的一个子任务,进程就是轻量级的进程
- 我们打开的浏览器这个进程,然后每打开的一个网页都是独立的线程,每个线程都可以执行一个子任务
为什么引入线程
- 有些进程可能需要“同时”做很多事,但是传统的进程只能串行得与执行一系列程序(比如你在使用QQ时,你登陆了账号,相当于创建了一个进程,你可以一边打字,一边视频聊天,这些是“同时”进行的,但是传统的进程是不能实现这样的功能,所以引入线程,加快并发的速度)
- 如果说在OS中引入进程的目的是为了使多个程序能够并发执行,比提高资源利用率和系统吞吐量,那么在OS中引入线程,是为了减少程序在并发执行时所付出的时间和空间开销,让OS具有更好的并发性(让进程并发得完成多种功能)
- 引入了线程之后,进程是除CPU之外系统资源的分配单元,如打印机,内存空间都是分配给进程的
- 引入了线程之后,不仅程序能够并发执行了,进程也能并发执行了,从而提高了系统并发度
线程和进程的比较
-
进程是OS资源分配的基本单位,线程是调度的基本单位
-
创建和消耗进程的开销远远比创建和消耗线程大的多,调度一个线程比进程快的多
-
进程包含线程,每个进程至少包含一个线程(主线程)
-
进程之间相互独立,不同的进程不会共享内存空间,但是同一个进程的线程会共享内存空间(共享进程的资源),线程几乎不拥有系统资源
-
在多CPU操作系统中,不同的线程可以去占用不同的CPU
-
同一进程的线程切换不会导致进程切换,但是不同进程的线程切换,会导致进程的切换
线程的优点
-
创建一个新线程的代价要比创建一个新进程小得多
-
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
-
线程占用的资源要比进程少很多
-
能充分利用多处理器的可并行数量,能够发挥多核CPU的优势
-
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
-
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
管程
- 也就是Monitor(监视器),也就是平时所说的锁
- Monitor其实是一种同步机制,它的义务是保证(同一时间只有一个线程可以访问被保护的数据和代码)
为什么引入管程?
- 虽然我们的记录型信号量能够真正的实现临界区的四大条件**(空闲让进 忙则等待 有限等待 让权等待 )** 但是在上面的例子中,我们知道消费者和生产者问题中,如果P的顺序反了(实现互斥的P操作一定要在实现同步的P操作之后),就会导致死锁的问题,PV操作编写困难,容易出错
- mutex为了实现缓冲区互斥
- empty是为了缓存区数量实现同步操作
- 所以提出了管程这种概念,这是一种高级同步机制
管程的组成及基本特征
组成
- 局部于管程的共享数据结构
- 对该数据结构进行操作的一组过程 (过程当成函数 管程当作类 共享数据结构当作类里面的属性)
- 对局部于管程的共享数据设置初始值的语句
- 管程有一个名字
基本特征
- 局部于管程的数据只能被局部于管程的过程所访问(封装思想)
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据
每次仅允许一个进程在管程内执行某个内部过程(实现了互斥访问)
管程实现生产者消费者问题
- 对于资源的互斥操作由编译器负责实现各进程互斥进入管程中的过程来实现
- 但是生产者和消费者的模型中还需要实现同步关系,我们需要用wait和signal来实现同步我们的full和empty有着对应的阻塞队列
Java中类似于管程的机制
public class startTest {
public static void main(String[] args) {
Object o=new Object();
new Thread(()->{
synchronized (o){
}
},"t2").start();
}
}
- JVM中同步是
基于进入和退出监视器对象(Monitor,管程对象)来实现的,每个对象实例都会有一个Monitor对象
, - Monitor对象会和Java对象一同创建并销毁,它底层是由C++语言来实现的。
- 执行线程就要要求先成功持有管程,然后才能执行方法,最后当方法完成时释放管程,在方法执行的期间线程拥有了管程,其他线程都无法再获取到同一个管程
异步和同步
以调用方角度来讲,如果
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
设计
- 多线程可以让方法执行变为异步的(即不要巴巴干等着)比如说读取磁盘文件时,假设读取操作花费了 5 秒钟,如果没有线程调度机制,这 5 秒 cpu 什么都做不了,其它代码都得暂停…
结论
- 比如在项目中,视频文件需要转换格式等操作比较费时,这时开一个新线程处理视频转换,避免阻塞主线程
- tomcat 的异步 servlet 也是类似的目的,让用户线程处理耗时较长的操作,避免阻塞 tomcat 的工作线程
- ui 程序中,开线程进行其他操作,避免阻塞 ui 线程
多线程的意义
什么叫多线程
- 多线程:多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
充分利用多核 cpu 的优势,提高运行效率。想象下面的场景,执行 3 个计算,最后将计算结果汇总。
计算 1 花费 10 ms
计算 2 花费 11 ms
计算 3 花费 9 ms
汇总需要 1 ms
- 如果是串行执行,那么总共花费的时间是 10 + 11 + 9 + 1 = 31ms
- 但如果是四核 cpu,各个核心分别使用线程 1 执行计算 1,线程 2 执行计算 2,线程 3 执行计算 3,那么 3 个
- 线程是并行的,花费时间只取决于最长的那个线程运行的时间,即 11ms 最后加上汇总时间只会花费 12ms
注意需要在多核 cpu 才能提高效率,单核仍然时是轮流执行
package Lambda;
public class ThreadNB {
private static final long count=10_0000_0000;
public static void main(String[] args) throws InterruptedException {
serial();
concurrent();
}
public static void serial(){
long start=System.nanoTime();
long a=0;
for (int i = 0; i < count; i++) {
a++;
}
long b=0;
for (int i = 0; i < count; i++) {
b++;
}
long end=System.nanoTime();
double allTime=(end-start)*1.0/1000/1000;
System.out.println("串行执行所用的时间"+allTime+"ms");
}
public static void concurrent() throws InterruptedException {
//并行实现20亿的累加
long start=System.nanoTime();
Thread thread1=new Thread(()->{
long a=0;
for (int i = 0; i < count; i++) {
a++;
}
});
thread1.start();//子线程进行十亿次累加
//主线程也进行10亿次累加
long b=0;
for (int i = 0; i < count; i++) {
b++;
}
// 等待子线程执行结束,主线程和子线程的加法操作都完成
// 等待子线程thread执行结束才能执行下面代码
thread1.join();//限制子线程执行完毕,才能运行下面的代码
long end=System.nanoTime();
double allTime=(end-start)*1.0/1000/1000;
System.out.println("并行耗费的时间为"+allTime+"ms");
}
}
//串行执行所用的时间955.9495ms
//并行耗费的时间为747.6655ms
- 理论上这里并发的执行速度应该是顺序执行的一倍
- 多线程的最大应用场景就是把一个大任务拆分为多个子任务(交给子线程),多个子线程并发执行,提高系统的处理效率,比如12306系统就是一个多线程程序,我们每个人其实都是一个线程,我们多个人可以同时登录系统买票,付款操作是一个非常耗时的操作,如果不是多线程,每个人买票就得像排队买票一样,依次进行,非常慢,有多线程(就可以趁着比如调整付款页面的时间去处理别人买票的操作,类似于有些任务场景需要 “等待 IO”, 为了让等待 IO 的时间能够去做一些其他的工作, 也需要用到并发编程)
为什么使用多线程(并发编程)
-
提升多核CPU的利用率:一般来说一台主机上的会有多个CPU核心,我们可以创建多个线程,理论上讲操作系统可以将多个线程分配给不同的CPU去执行,每个CPU执行一个线程,这样就提高了CPU的使用效率,如果使用单线程就只能有一个CPU核心被使用。
-
比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分 。
-
简单来说就是:
-
充分利用多核CPU的计算能力;
-
方便进行业务拆分,提升应用性能
-
并发编程有什么缺点
- 并发编程的目的就是为了能提高程序的执行效率,提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:
内存泄漏
、上下文切换
、线程安全
、死锁
等问题
结论
-
单核 cpu 下,多线程不能实际提高程序运行效率,只是为了能够在不同的任务之间切换,不同线程轮流使用cpu ,不至于一个线程总占用 cpu,别的线程没法干活
-
多核 cpu 可以并行跑多个线程,但能否提高程序运行效率还是要分情况的有些任务,经过精心设计,将任务拆分,并行执行,当然可以提高程序的运行效率。但不是所有计算任务都能拆分(参考后文的【阿姆达尔定律】)也不是所有任务都需要拆分,任务的目的如果不同,谈拆分和效率没啥意义
-
IO 操作不占用 cpu,只是我们一般拷贝文件使用的是【阻塞 IO】,这时相当于线程虽然不用 cpu,但需要一直等待 IO 结束,没能充分利用线程。所以才有后面的【非阻塞 IO】和【异步 IO】优化
查看进程线程的方法
windows
- 任务管理器可以查看进程和线程数,也可以用来杀死进程
- tasklist 查看进程
- taskkill 杀死进程
linux
- ps -fe 查看所有进程
ps -fT -p <PID>
查看某个进程(PID)的所有线程
- kill 杀死进程
- top 按大写 H 切换是否显示线程
top -H -p <PID>
查看某个进程(PID)的所有线程
Java
- jps 命令查看所有 Java 进程
jstack <PID>
查看某个 Java 进程(PID)的所有线程状态- jconsole 来查看某个 Java 进程中线程的运行情况(图形界面)
线程上下文切换
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完,JVM 暂时放弃 CPU 操作(基于时间片轮转调度的 JVM 操作系统不会让线程永久放弃 CPU,或者说放弃本次时间片的执行权)
- 当前运行线程因为某些原因进入阻塞状态,例如阻塞在 I/O 上。
- 垃圾回收
- 有更高优先级的线程需要运行
- 当前运行线程结束,即运行完 run()方法里面的任务。
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
上下⽂是指某⼀时间点 CPU 寄存器和程序计数器的内容,也就是线程(进程)执行的环境, 当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念
- 就是程序计数器(Program Counter Register),它的作用是记住下一条 JVM 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
举例说明 线程A - B
- 1.先挂起线程A,将其在cpu中的状态保存在内存中。
- 2.在内存中检索下⼀个线程B的上下⽂并将其在 CPU 的寄存器中恢复,执⾏B线程。
- 3.当B执⾏完,根据程序计数器中指向的位置恢复线程A。
Context Switch 频繁发生会影响性能
用户线程和守护线程
Java线程分为用户线程和守护线程
-
一般情况下不做特别说明配置,默认都是用户线程
-
用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
-
守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”
- GC垃圾回收线程就是经典的守护线程
- 一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
-
线程的daemon属性为
- true表示是守护线程
- false表示是用户线程。
public class DaemonDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t 开始运行,"
+(Thread.currentThread().isDaemon() ? "守护线程":"用户线程"));
while (true){
}
},"t1");
//线程的daemon属性为true表示是守护线程,false表示是用户线程
//---------------------------------------------
t1.setDaemon(true);
t1.start();
//3秒后主线程再运行
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("----------main线程运行完毕");
}
}
两种情况
- 未加
t1.setDaemon(true);
,默认是用户线程,t1继续运行,所以灯亮着 - 加了
t1.setDaemon(true);
是守护线程,当用户线程main方法结束后t1自动退出了
守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可退出了。假如当系统只剩下守护线程的时候,Java虚拟机会自动退出。
setDaemon(true)方法必须在start()之前设置,否则报IIIegalThreadStateException异常
JAVA线程
Java 主类名称 启动的是JAVA进程
类中的主方法是这个类的主线程 ,线程是操作系统中的概念 . 操作系统内核实现了线程这样的机制 , 并且对用户层提供了一些 API 供用户使 用( 例如 Linux 的 pthread 库 ). Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进行了进一步的抽象和封装
Java线程创建的方法
- 继承Thread类,覆写run方法
- 覆写Runnable接口,覆写run方法
- 覆写Callable接口,覆写call方法
- 使用线程池创建线程
继承Thread类
- 一个子类继承Thread类
- 覆写run方法
class MyThread extends Thread{
@Override
public void run() {
System.out.println("线程运行");
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
// 匿名内部类写法 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
// 构造方法的参数是给线程指定名字,推荐
Thread t1 = new Thread("t1") {
@Override
// run 方法内实现了要执行的任务
public void run() {
// 要执行的任务
}
};
// 启动线程
t.start();
使用Runnable接口配合Thread
把【线程】和【任务】(要执行的代码)分开
- Thread 代表线程
- Runnable 可运行的任务(线程要执行的代码)
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
// 创建线程对象
Thread t = new Thread( runnable );
// 启动线程
t.start();
// 创建任务对象
Runnable task2 = new Runnable() {
@Override
public void run() {
log.debug("hello");
}
};
// 参数1 是任务对象; 参数2 是线程名字,推荐
Thread t2 = new Thread(task2, "t2");
t2.start();
//匿名内部类写法
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程运行");
}
}, "t1");
//Lambda写法
Thread thread = new Thread(() -> System.out.println("线程运行"), "t1");
thread.start();
- Lambda只能用来实现函数式接口,只有一个抽象方法的接口,具体详情看其http://t.csdn.cn/gI076
- 用 Runnable 更容易与线程池等高级 API 配合
- 用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
- 组合优先于继承
FutureTask 配合 Thread
- 先实现Callable接口
- 覆写核心方法call方法
- 创建相应的FutureTask类来接收Callable的返回值
- 将FutureTask的对象传入Thread类的对象
FutureTask<Integer> FutureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
});
Thread thread1 = new Thread(FutureTask);
thread1.start();
System.out.println(FutureTask.get());
-
Callable实现接口的线程就是带有返回值,返回值要用FutureTask类的对象来接收
-
Callable 和 Runnable 相对 , 都是描述一个 " 任务 ". Callable 描述的是带有返回值的任务 ,Runnable 描述的是不带返回值的任务
-
Callable 通常需要搭配 FutureTask 来使用 . FutureTask 用来保存 Callable 的返回结果 . 因为 Callable 往往是在另一个线程中执行的 , 啥时候执行完并不确定
runnable 和 callable 有什么区别
相同点:
- 都是接口 都可以编写多线程程序 都采用Thread.start()启动线程
主要区别:
-
Runnable 接口 run 方法无返回值(直接用Thread对象来接收这个Runnable接口的对象);
-
Callable 接口 call 方法有返回值,是个泛型,需要用FutureTask对象配合可以用来获取异步执行的结果(用FutureTask对象来接收Callable接口的,然后用Thread对象来接收FutureTask的对象)
-
Runnable 接口 run 方法只能抛出运行时异常,且无法捕获处理;Callable 接口 call 方法允许抛出异常,可以获取异常信息 注:Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
什么是 FutureTask
FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类的对象,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算,完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。
- 关于详情会在后面进行讲解
线程类的构造方法、静态块是被哪个线程调用的
- 请记住:线程类的构造方法、静态块是被 new这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
- 如果说上面的说法让你感到困惑,那么我举个例子,main 函数中 new 了 Thread2,那么:
- (1)Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
使用线程池
public class ThreadDemo1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程运行");
}
});
}
}
- 线程池的知识点在后面会详细说明
Java 中用到的线程调度算法
(Java实现的是抢占式,Java是由JVM中的线程计数器来实现线程调度)
分时调度模型和抢占式调度模型。
- 分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU的时间片这个也比较好理解。
- Java 使用的线程调使用抢占式调度, Java 中线程会按优先级分配 CPU 时间片运行, 且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
run和start的区别
start() 启动一个新线 程,在新的线程
运行 run 方法 中的代码
- start 方法只是让线程进入就绪,里面代码不一定立刻 运行(CPU 的时间片还没分给它)。每个线程对象的 start方法只能调用一次,如果调用了多次会出现 IllegalThreadStateException
- new 一个 Thread,线程进入了新建状态。调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到CPU资源后就可以开始运行了(运行态)。 start() 会执行线程的相应准备工作,然后自动执行run() 方法的内容,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码;
public class ThreadDemo2 {
public static void main(String[] args) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"开始运行");
},"t1").start();
}
}
//t1开始运行
public class ThreadDemo2 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始运行");
}, "t1");
thread.start();
thread.start();
}
}
t1开始运行
Exception in thread "main" java.lang.IllegalThreadStateException
at java.lang.Thread.start(Thread.java:708)
at com.lsc.day10.ThreadDemo2.main(ThreadDemo2.java:17)
run() 新线程启动后会调用的方法
- 如果在构造 Thread 对象时传递了 Runnable 参数,则 线程启动后会调用 Runnable 中的 run 方法,否则默 认不执行任何操作。但可以创建 Thread 的子类对象,来覆盖默认行为
- run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有多线程的特征,
public class ThreadDemo2 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始运行");
}, "t1");
thread.run();
}
}
//main开始运行