java多线程
1- 进程
进程:就是一个运行的程序,例如:我们运行多个QQ,那么就会产生多个QQ的进程。
进程是运行在内存中的,每开启一个进程,操作系统就会为每个进程分配内存空间
进程生命周期:产生、存在、消亡
1-1 线程
线程是由进程创建的,是进程的一个实体
一个进程可以开多个线程,例如:我们用迅雷下载一部电影的时候,就会产生一个线程。当然也可能多个电影同时下载。那每个电影的下载就是一个线程(多线程)。
1-2 概念
单线程:一个进程只能开启一个线程,就叫单线程
多线程:一个进程开启多个线程,就叫多线程
并发:并发主要讲的是单核CPU交替执行多个任务(进程)
并行:并行主要讲的是多核CPU同时执行多个任务(进程)
并行并发同时存在:CPU的核数少于任务(进程程)的数量,就会在并行的同时并发
假如有一个两核CPU,同时执行两个任务(进程),这就是并行。如果我们又来了一个任务(进程),那就有一个CPU会交替(并发)的执行两个任务(进程),另一个CPU执行另一个任务。所以两个CPU是并行的,而其中一个CPU在并行的同时是并发的
2- 守护线程
谁是谁的守护线程?
守护线程是在哪个线程上创建的,就是哪个线程的守护线程
不是在哪个线程启动就是哪个线程的守护线程
使用setDaemon方法设置这线程是否是守护线程
thread.setDaemon(true);
例如:
main线程(用户线程)
t1(守护线程)
t2(守护线程)
t2在t1中创建、t1在main中创建
关系如下:t2是t1的守护线程、t1是main的守护线程。
main结束t1守护线程也就结束,t1线程结束t2守护线程也跟着结束。
注意:当用户线程结束后,守护线程不是立马停止的。守护线程可能会在终止之前执行一段时间。
如下:
public class MyThread extends Thread{
// t2线程
Thread t2;
public static void main(String[] args) throws InterruptedException {
System.out.println("main线程开始了--------------");
MyThread t1 = new MyThread();
t1.start();
// 这里停个0.1秒;如果不停,启动t2线程会报空指针异常,因为t1线程还没有把t2创建赋值,main线程就去启动t2线程。
Thread.sleep(100);
t1.t2.start();
System.out.println("main线程结束了--------------");
}
@Override
public void run() {
t2= new Thread(new MyThread(){
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
// try {
// Thread.sleep(1000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("t2线程执行了!!!");
}
}
});
// 设置成守护线程
t2.setDaemon(true);
for (int i = 0; i < 5; i++) {
// try {
// Thread.sleep(500);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
System.out.println("t1线程运行了!");
}
System.out.println("t1线程结束了--------------");
}
}
结果:
会发现明明t1用户线程已经结束了,但t2守护线程执行了一段才结束,不过并没有打印5000条日志,所以就验证了上面那一段话。
解决方法:就是打印之前停一下,就是上面注释的代码
3- 线程的生命周期
线程中有6大状态,用枚举来实现。
在Thread类中有State内部枚举,表示的是线程的每种状态
// 源码
public enum State {
NEW, // 未被start()调用前的状态
RUNNABLE, // 准备就绪,准备被系统调用
BLOCKED, // 堵塞
WAITING, // 无期限等待
TIMED_WAITING, // 有期限等待
TERMINATED; // 线程终止的状态
}
下面是运行时的状态流程图(多出的一个是Running运行)
4- CPU多级缓存架构
众所周知CPU的速度是最快的,其次就是主存。
但是两者之间速度还是有一定差距,为了不让主存拖累CPU,就诞生了CPU多级缓存
三种缓存
- 一级缓存(L1):是位于CPU核心内部的最快速度的缓存,用于存储最常用的数据和指令。它分为指令缓存(Instruction Cache)和数据缓存(Data Cache),并且与CPU核心紧密结合,速度非常快,但容量较小。
- 二级缓存(L2):位于CPU核心和主内存之间,容量比一级缓存大,速度较慢。它的作用是作为一级缓存的备份,并且可以容纳更多的数据。
- 三级缓存(L3):位于多个CPU核心之间共享,容量更大但速度更慢。它的作用是为多个核心提供共享的数据和指令,并减少核心之间的竞争。
速度:L1 > L2 > L3 > 主存
容量:主存 > L3 > L2 > L1
造价:L1 > L2 > L3
一级和二级缓存是核独用的。三级缓存是核共享的。
去任务管理器可以查看自己电脑的三个缓存,下面是我在学校机器上截图。
要吐槽一下:学校电脑CPU比我电脑CPU都高(老电脑:i5 7代哈哈哈)
4-1 局部性原理
CPU去主存中读取一个值的时候,CPU不是只读一个数据,而是一片数据(现在是64个字节,以后可能会变),这一片数据就叫缓存行。
CPU不会只读取目标值,而且是把目标数据左右相邻的数据一起读取到CPU的L3(三级缓存)、L2、L1中
疑问:为什么要把相邻的数据一起取出来?
数组就是一个很好的例子:因为数组在内存中是一个连续的空间,你读取下标为5的数据时,CPU可能就会把下标为3到7的数据一起读取出来
缓存行是缓存中最小的可读写单位,在现代CPU中通常是64字节。它是为了利用局部性原理而设计的,当CPU访问某个内存地址时,会将该地址周围的连续字节块一起加载到缓存行中。这样当CPU需要访问相邻地址时,可以直接在缓存行中完成,提高访问速度。
4-1-1 优点
命中率高
4-1-2 缺点
每一次的读取变慢了,本来就读取4个字节,结果读取64个字节
4-2 CPU缓存导致的可见性的问题(不一致性)
P1:核1
P2:核2
D:数据(int i = 1)
如:有一个两核CPU,两个核都要去给D数据加一。CPU会去主存中读取到L3,当P1修改D时,会去L3中读取到自己的缓存中(L1、L2),P1修改后会把数据依次刷到L3中,流程:L1 > L2 > L3;当P1刷回L2还没有刷回L3时,L3中的数据i = 1,这个时候P2也要给D加一,P2会去L3中读取,读取到的是修改前的。所以P2修改后的结果也是 i = 2;P1修改后L3中的i = 2,当P2刷回L3时,也是把i = 2的结果刷回去,这就造成了可见性的问题
问题总结:在一个CPU修改了内存数据的时候,其它CPU是不知道的,所以导致一个CPU改了,另外一个CPU看不见,从而使用了旧的数据,导致了程序不正确的结果。
解决方案:
很多CPU厂家都有自己的一个解决方案:比如英特尔的MESI协议,有兴趣的可以去了解一下
4-3 乱序执行
CPU的乱序执行:是一种优化技术,用于提高计算机指令执行的效率。在传统的顺序执行中,指令按照程序的顺序依次执行。但是,在乱序执行中,CPU可以根据指令之间的依赖关系,灵活地重排序指令的执行顺序(简称:指令重排),以充分利用CPU的资源,提高指令流水线的吞吐率。
乱序执行主要是通过指令重排实现的
5- java内存模型
JMM就是Java内存模型(Java Memory Model),是Java虚拟机规范中定义的一种内存模型,用于描述线程之间的共享变量的可见性和有序性。JMM决定了一个线程如何和主内存交互,以及如何和其他线程进行通信。
JMM主要包括以下几个关键概念:
-
主内存(Main Memory):主内存是所有线程共享的内存区域,包含所有的变量。主内存是对所有线程可见的。
-
工作内存(Working Memory):工作内存是每个线程独立拥有的内存区域,用于保存该线程使用到的变量的副本。
-
内存间交互操作:JMM定义了一系列规则,用于控制线程如何将变量从主内存同步到工作内存以及如何将变量从工作内存同步回主内存。这些操作包括读取、写入、锁定和解锁等。
JMM的三个特征:原子性、可见性和有序性:JMM提供了一些保证,以确保多线程环境下的变量操作满足原子性、可见性和有序性。原子性指一个操作不可中断,要么全部执行成功,要么全部不执行;可见性指一个线程对共享变量的修改对其他线程是可见的;有序性指程序执行的结果是按照一定的顺序来进行。
总结来说,JMM定义了线程之间如何进行内存交互,保证了多线程环境下共享变量的可见性和有序性。
5-1 as-if-serial语义
as-if-serial语义的意思指:不管指令怎么重排序,程序(单线程)的执行结果不能被改变。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被重排序。
5-1-1 数据依赖关系
什么是依赖关系:比如在给一个变量赋值时,用到了其他的变量。那它们之间就存在依赖关系。
代码如下:
// 存在依赖关系如下:
// b变量赋值的时候用到了a变量,存在依赖关系
int a = 0;
int b = (++a) + 1;
---------------------------------------------------------------
// 不存在依赖关系如下:
int a = 0;
int b = 2;
5-2JMM模型中存在的问题
5-2-1 指令重排
指令重排:是编译器或处理器的一种优化技术,它可以改变指令的执行顺序,以提高程序的性能或减少资源消耗。
指令重排遵循as-if-serial语义
在现代计算机中,指令通常以高级语言编写,并通过编译器转换为机器代码。编译器或处理器可以根据指令之间的依赖关系对指令进行重新排序,以充分利用计算资源并最大限度地提高程序的执行效率。
指令重排可以有多种形式,包括乱序执行、流水线处理和超标量处理等。
简单理解:
(顺序执行)
我们要做一个美脑枣(就是下面那个)
具体流程是:拿工具打开核桃 > 放下工具 > 拿出核桃仁 > 拿工具切开红枣\ > 放下工具 > 把核桃仁放进去
(乱序执行)
当我们要做一堆的时候,如果还是按照上面的流程做一个美脑枣要拿起放下工具两次(顺序执行),明显就不合适了。
所以就出了另一种流程:拿工具打开所有核桃 > 放下工具 > 拿出所有核桃仁 > 拿工具切开所有红枣\ > 放下工具 > 把所有的核桃仁和红枣装一起
5-2-1-1 指令重排造成的问题
阅读下面代码
public class Temp {
private static int a = 0;
private static int b = 0;
private static int x = 0;
private static int y = 0;
private static Long number = 0L;
public static void main(String[] args) throws InterruptedException {
// 记录当前时间
Long time = System.currentTimeMillis();
while (true){
Thread t1 = new Thread(
// lambda表达式
() -> {
a = 1;
x = b;
});
Thread t2 = new Thread(
// lambda表达式
() -> {
b = 1;
y = a;
});
// 启动线程
t1.start();
t2.start();
// 等待线程结束
t1.join();
t2.join();
System.out.println("执行了:" + (++number) + "次");
if (x == 0 && y == 0) {
// 记录用时多久结束(毫秒)
time = System.currentTimeMillis() - time;
System.out.println("x =" + x + "y = " + y);
System.out.println("循环结束!!!耗时:" + time + "毫秒");
break;
}
// 还原
a = 0;
b = 0;
x = 0;
y = 0;
}
}
}
结果:
是不是觉得很奇怪,明明这个是一个死循环,为什么x和y都会等于0?
分析:
造成这个的主要原因就是指令重排了!
因为两个线程里面的run方法都不存在依赖关系,所以就有可能会重排。
所以两个run方法进行重排,当碰巧把x = b和y = a两段代码分别重排到各个run方法的第一位。就造成了x和y都是0的结果,然后循环就结束了。
5-2-1-2 解决方案(volatile关键字、内存屏障)
volatile关键字
用法:[private] [volatile] [static] int a = 0;
volatile关键字底层采用的是内存屏障来实现的
// 给a和b变量加上volatile关键字就可以解决
private volatile static int a = 0;
private volatile static int b = 0;
内存屏障
内存屏障:是一种同步机制,用于控制指令的执行顺序以及对内存的访问顺序。在多线程编程中,由于线程间的并发执行,可能会出现数据竞争的问题,导致程序出现不确定的行为。内存屏障可以通过限制指令执行和内存访问的顺序来解决这些问题。
内存屏障可以分为两类:
- Load Barrier(读屏障):确保在读取某个内存位置的值之前,所有之前的读写操作都已经完成,并且将读取的值从缓存刷新到主存中。
- Store Barrier(写屏障):确保在写入某个内存位置的值之前,所有之前的读写操作都已经完成,并且将写入的值从缓存刷新到主存中。
内存屏障的作用有以下几点:
- 保证指令的顺序性:在执行内存屏障之前的指令都必须要完成,才能执行内存屏障之后的指令。
- 保证对共享数据的可见性:内存屏障可以确保某个线程对共享数据的修改对其他线程是可见的。
- 防止编译器优化:内存屏障可以防止编译器将指令重新排序或优化,确保程序的执行结果符合预期。
如图:
像一个屏障把多个指令分隔,分隔成一段一段的,当上一段完成了,才能完成下一段。段里面的指令也是乱序执行的
5-2-2 可见性(问题一)
JMM中一个线程都有独自的工作内存,当你修改了一个值刷入主存,其它线程是感知不到的,所以其它线程会继续使用当前工作内存里的值;
感觉跟CPU的核于核之间可见性问题差不多。
public class Temp {
public static boolean isOver = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
// for (;isOver;) {
// }
while (isOver) {
}
System.out.println("你能看见我吗?");
}).start();
Thread.sleep(2000);
isOver = false;
System.out.println("已经修改-------------------------------------------------");
Thread.sleep(5000);
System.out.println("5秒过去了");
}
}
结果:
会发现Main线程已经把isOver变量修改成false,而t1线程还在执行。
但是当你给循环体加入一行代码,就会发现可见性的问题不见了
public class Temp {
public static boolean isOver = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
// for (;isOver;) {
// System.out.println("t1");
// }
while (isOver) {
System.out.println("t1");
}
System.out.println("你能看见我吗?");
}).start();
Thread.sleep(2000);
isOver = false;
System.out.println("已经修改-------------------------------------------------");
}
}
结果:
因为编译器对空的循环做优化,将循环完全优化掉,导致没有任何读取共享变量的操作,所以就导致可见性的问题。
5-2-3 对象半初始化(问题二)
创建对象有三部分:
1、分配堆内存空间
2、初始化(构造)方法
3、栈中变量指向对象的堆内存空间
因为指令重排的原因,第二和第三步的执行顺序可能会颠倒的。
对象半初始化:就是运行了第一步和第三步,但没有执行初始化方法,对象的属性都还没有赋值,这就是处在半初始化的状态。
在半初始化状态,其他线程需要用到这个对象里面的属性,此时就可能会出现空指针的异常
对象半初始化:可能会导致线程安全问题、空指针异常等
5-2-4 线程争抢(问题三)
线程争抢感觉还是可见性的问题,当你修改一个值时,其他线程是无感知的。
代码如下:使用两个线程对同一个资源进行自加1的方法,每个线程都加10万次,按理说总值是20万,但实际的值会比这少很多
public class MyTemp {
public static int coun = 0;
// coun自加一
public static void add() {
coun++;
}
public static void main(String[] args) throws InterruptedException {
new Thread(() ->{
for (int i = 0; i < 100000; i++) {
add();
}
System.out.println("结束");
}).start();
new Thread(() ->{
for (int i = 0; i < 100000; i++) {
add();
}
System.out.println("结束");
}).start();
Thread.sleep(500);
System.out.println(coun);
}
}
6- 线程安全的实现方法
(1)、数据不可变:就是要把共享的数据设置为不可修改的,如用final关键字修饰
(2)、互斥同步:就是把资源设置成互斥同步的,一个线程在用的时候,其他线程只能堵塞在那个。(悲观的并发策略)
(3)、非堵塞同步:就还是把资源是互斥同步的,一个线程在用的时候,另一个线程不是挂起,而是一直尝试使用。像循环一样一直在尝试使用,直到使用成功,循环才结束。(乐观并发策略)
(4)、无同步方案:就是把要共享的数据,变成一个个线程的局部数据,这样就解决线程安全的问题()。
(4)、无同步方案:就是把要共享的数据,变成一个个线程的局部数据,这里引出一个ThreadLocal类
6-1 ThreadLocal类
ThreadLocal叫做线程变量,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
ThreadLocall里面维护的是一个Map对象,Key就是线程对象,Value就是我们设置进去的值
常用方法:get、set、remove方法
get方法:就是根据当前线程对象,去维护的Map对象获取Value
set方法:就是根据当前线程对象,去维护的Map对象设置Value
remove方法:就是根据当前线程对象,去维护的Map对象把Value删除,默认值Null
使用案例:
public class MyTemp {
// 共享数据
public static int coun = 0;
// 线程变量
public static final ThreadLocal<Integer> local = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() ->{
// 设置
local.set(coun);
for (int i = 0; i < 1000; i++) {
// 获取
local.set(local.get() + 1);
System.out.println("T1--------"+local.get());
}
// 删除
local.remove();
System.out.println("结束");
}).start();
new Thread(() ->{
// 设置
local.set(coun);
for (int i = 0; i < 1000; i++) {
// 获取
local.set(local.get() + 1);
System.out.println("T2--------"+local.get());
}
// 删除
local.remove();
System.out.println("结束");
}).start();
}
}
7- synchronization关键字
synchronized锁分为两种:一种是锁对象(this)、一种是锁类(class)的
synchronized的五种用法:
public class MyTemp {
// 在 用法五 使用
public static final MyTemp obj = new MyTemp();
// 用法一,锁的是this(当前对象)
public synchronized void say() {
System.out.println(0);
System.out.println(Math.random());
}
// 用法二,锁的是MyTemp这个类
public static synchronized void say01() {
System.out.println(0);
System.out.println(Math.random());
}
// 用法三,锁的是this(当前对象)
public void say02() {
synchronized (this) {
System.out.println(0);
System.out.println(Math.random());
}
}
// 用法四,锁的是MyTemp这个类
public static void say03() {
synchronized (MyTemp.class){
System.out.println(0);
System.out.println(Math.random());
}
}
// 用法五,锁的是obj对象
public static void say04() {
synchronized (obj){
System.out.println(0);
System.out.println(Math.random());
}
}
}
无锁、偏向锁、轻量锁、重量锁
8- 死锁
死锁代码如下:
public class MyTemp {
public static final Object obj01 = new Object();
public static final Object obj02 = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 请求锁一
synchronized(obj01) {
System.out.println(Thread.currentThread().getName()+"获取了一号锁!");
// 休眠100毫秒,确保两个线程都拿到了锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 请求锁二
synchronized (obj02) {
System.out.println(Thread.currentThread().getName()+"获取了二号锁!");
}
}
},"T1").start();
new Thread(() -> {
// 请求锁二
synchronized(obj02) {
System.out.println(Thread.currentThread().getName()+"获取了二号锁!");
// 休眠100毫秒,确保两个线程都拿到了锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 请求锁一
synchronized (obj01) {
System.out.println(Thread.currentThread().getName()+"获取了一号锁!");
}
}
},"T2").start();
}
}
结果:
9- 锁重入
10- 方法
10-1 wait和notify方法
wait方法:释放CPU资源,当前线程释放对象锁的拥有权,在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,当前线程处于等待状态。
notify方法:唤醒在此对象锁上等待的单个线程。如果有多个线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并且根据实现进行选择。
notifyAll方法:唤醒在此对象锁上等待的所有个线程。让所有线程进行一次争抢,没抢到的线程继续等待。
上面方法都来自Object类
调用当前线程 noitfy() 后,等待获取对象锁的其他线程(可能有多个)不会立即从 wait() 处返回,而是需要调用 notify() 的当前线程释放锁(退出同步块)之后,等待线程才有机会从 wait() 返回
注意:sleep方法只释放CPU资源,而不释放锁。
public class MyTemp {
public static final Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized(obj) {
System.out.println(Thread.currentThread().getName()+"获取了锁!");
System.out.println(Thread.currentThread().getName()+"进入等待状态!");
try {
obj.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"结束等待状态!");
System.out.println(Thread.currentThread().getName()+"结束!--------");
}
},"T1").start();
new Thread(() -> {
synchronized(obj) {
System.out.println(Thread.currentThread().getName()+"获取了锁!");
System.out.println(Thread.currentThread().getName()+"唤醒其它等待线程!!");
obj.notify();
System.out.println(Thread.currentThread().getName()+"结束!---------");
}
},"T2").start();
}
}
结果:
下面是我用另一种synchronized的用法写的。下面使用了双冒号(::)运算操作符
双冒号运算操作符是lambda表达式的一种简写。
使用lambda表达式会创建匿名函数, 但有时候需要使用一个lambda表达式只调用一个方法(不做其它), 所以这才有了双冒号运算操作符!
public class MyTemp01 {
public synchronized void myWait() {
System.out.println(Thread.currentThread().getName()+"获取了锁!");
System.out.println(Thread.currentThread().getName()+"进入等待状态!");
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"结束等待状态!");
System.out.println(Thread.currentThread().getName()+"结束!--------");
}
public synchronized void myNotify() {
System.out.println(Thread.currentThread().getName()+"获取了锁!");
System.out.println(Thread.currentThread().getName()+"唤醒其它等待线程!!");
this.notify();
System.out.println(Thread.currentThread().getName()+"结束!---------");
}
public static void main(String[] args) {
MyTemp01 obj = new MyTemp01();
// 双冒号(::)运算操作符是lambda表达式的一直简写
new Thread(obj::myWait,"T1").start();
// 双冒号(::)运算操作符是lambda表达式的一直简写
new Thread(obj::myNotify,"T2").start();
}
}
10-2 yield方法(不常用)
yieid方法:不会释放锁,会释放CPU的执行权,但是依然保留了CPU的执行资格。
yield方法来自Thread类(静态方法)
例如:有多个线程争抢CPU,A线程里调用了yieid方法。
A线程争抢到了CPU的执行权后运行到yieid方法处会放弃CPU的执行权,然后会再去争抢CPU执行权,如果没有抢到,下一次会继续争抢,直到抢到为止,然后执行yieid方法后面的方法。
public class MyTemp01 {
public static int a = 0;
public static int b = 0;
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("线程进行争抢-----");
Thread.yield();
System.out.println("线程争抢到了!!!");
System.out.println(Thread.currentThread().getName()+":"+ ++a);
}
},"T1").start();
new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName()+":"+ ++b);
}
},"T2").start();
}
}
T1要争抢到两次才能运行完一次,而T2只要争抢到一次就能运行完一次
10-3 join方法
join是线程方法,程序会堵塞在这里等着这个线程执行完毕,才接着向下执行。
join方法属于Thread对象
public class MyTemp01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {}
System.out.println(Thread.currentThread().getName() + "线程结束了!");
}, "T1");
t1.start();
t1.join();
System.out.println(Thread.currentThread().getName()+"线程结束了!");
}
}
结果:
10-4 Sleep方法
让当前线程睡眠,单位为毫秒。线程睡眠的时候会释放CPU资源,但不会释放锁。睡眠的时间一过,立马夺回CPU执行权。
sleep方法属于Thread类(静态方法)
public class MyTemp01 {
public static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized(obj) {
System.out.println("T1线程开始了!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("T1线程结束了!!!");
}
},"T1").start();
new Thread(() -> {
synchronized (obj) {
System.out.println("T2拿到了obj的对象锁!");
}
},"T2").start();
}
}
结果:
10-4 interrupt方法
interrup方法用于打断休眠、等待状态下的线程,打断后会报InterruptException异常
interrupt方法属于Thread对象
public class MyTemp01 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
System.out.println("T1线程开始了!");
try {
Thread.sleep(200000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T1线程被打断了!!!");
},"T1");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
}
结果: