1.0 Thread类
当点击运行程序的时候,就会创建一个java进程,这个进程中至少包含至少一个线程,这个线程也叫做主线程,也就是负责执行main方法的线程
//官方的Thread来自java.lang这个包,java.lang比较特殊,这里的类不需要手动import,可以默认使用
class MyThread extends Thread{
//这里的run相当于线程的入口方法,线程具体跑起来之后要做啥事都是通过 run来描述的
@Override
public void run() {
while(true){
System.out.println("i am thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
//start就是在创建线程,这个操作就会在底层调用OS提供的"创建线程"的API,同时就会在操作系统内核里创建出对应的pcb结构,并且加入 到对应的链表当中.
//这个新创建出来的线程就会参与到cpu的调度中,这个线程接下来执行的工作就是执行run内的内容
myThread.start();
//run只是上面入口方法(普通方法),并没有调用系统api,也没有创建出真正的线程来,还在main这个线程中
//myThread.run()
while (true){
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
2.0 观察线程的方法
- idea调试器(这里不做演示)
- jconsole (官方在jdk中给使用者的调试工具,在java/bin/jconsole.exe 内)
- 通过idea找java路径,File->Project Structure->SDKS
- jconsole本质也是java写的程序,也是进程
- java的线程不制止是主线程(main)和自己创建的线程(Thread-0/-1/-2),其他剩下的线程就是jvm自带的,需要完场其他的工作
- 线程调用栈,记录方法间的调用关系,尤其是程序卡死时的时候,这里查看一下每个线程的调用栈就可以大概知道是在哪个代码中卡死的
- c语言中Windows版本的Sleep(ms)(Windows.h), Linux版本的为sleep(s) (unistd.h) [不同os提供的api就不同],但是java中就是把上述的函数进行了封装,在windows版本的jvm就调用winodws的Sleep
- sleep有异常要手动处理的受查异常
- 重写函数run(这个方法)中的sleep不能throw,只能try-catch,应为对于父类的run来说就没有throws xxx的设定(没申明异常),如果main函数内throws了异常(声明了异常),则main函数类就可以throw或者压根不写throw
- sleep中线程就变成 阻塞状态,当时间到了,系统就会唤醒线程,并且回复两个线程的调度,当两个线程都醒了,谁先调度,可视为**“随机”,**这样随即调度的过程称为,抢占式执行
3.0 创建Thread的多种方法
两种概念方法
- 创建一个类,继承Thread,重写run方法
- 创建一个类,实现(interface)Runnable, 重写run方法 (java中interface 通常会使用形容词性的词来描述 如:able)
基础方法
继承Thread,重写run方法 (基础)
//官方的Thread来自java.lang这个包,java.lang比较特殊,这里的类不需要手动import,可以默认使用
class MyThread extends Thread{
@Override
public void run() {
while(true){
System.out.println("i am thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
MyThread myThread = new MyThread();
myThread.start();
while (true){
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
继承Thread和接口Runnable
-
继承 Thread 这里是直接把要完成的工作,放到继承Thread的类中的run方法中
-
Runnable 这里,则是分开的,要把完成的工作放到Runnable中,再让Runnable和Thread 配合(这里把线程要执行的任务和线程本身进一步解耦合了
-
想用编程的方式来完成某个工作,使用Runnable来描述这个工作的具体细节,若使用多线程的方式,可以用Runnable搭配线程使用,如果使用线程池的方式,就用可以用Runnable搭配线程池的方式使用,使用携程的方式,也可以使用Runnable搭配携程
-
Thread相当于把某个并发式编程绑定到线程使用,而Runnable则是灵活的可以使用多种方式去编程
class MyRunnable implements Runnable{
@Override
public void run() {
while(true){
System.out.println("hello thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Demo2 {
public static void main(String[] args) throws InterruptedException {
//把实现Runnable接口的对象当成参数传给Thread的构造函数
MyRunnable myRunnable = new MyRunnable();
Thread t =new Thread(myRunnable);
t.start();
while(true)
{
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
进阶方法
继承Thred+匿名内部类 (进阶)
//通过匿名内部类和Thread来创建
public class Demo3 {
public static void main(String[] args) throws InterruptedException {
//相当有个匿名的子类继承了Thread,重写了run,并且向上转型用Thread来继承这个子类
Thread t= new Thread(){
@Override
public void run() {
while(true){
System.out.println("i am thread");
//这里相当于一个类继承Thread 而Thread.run函数没有声明异常,所以一定要try,catch
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t.start();
while(true){
System.out.println("i am main");
sleep(5000);
}
}
}
实现runnable+基于匿名内部类 (进阶)
//runnable + 匿名内部类
public class Demo4 {
public static void main(String[] args) throws InterruptedException {
//创建匿名类实现runnable接口 重写run方法 实例化,传给Thread构造方法
Thread t = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("i am thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while(true){
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
注意点:
- java想实现完全面向对象的编程---->c++不完全面向对象,他的函数能独立于对象---->java的函数不能脱离函数/类------>创建线程这个场景中,更重要的是run 函数/方法具体是啥
直接使用lambda实现runnable
//用lambda
public class Demo5 {
public static void main(String[] args) throws InterruptedException {
//理解1:lambda表达式创建一个匿名函数,实现了runnable接口,重写run方法,且此时的lambda是是个回调函数,是在线程创建成功后,才真正执行
//理解2:也可以理解成lambda实现了一个匿名内部类 实现了 runnable接口,重写了run
//函数式接口:java本省不允许函数脱离类/对象 来使用.lambda相当于特事特办,运用了java的"函数式接口"这样的概念
Thread t = new Thread(()->{
while(true){
System.out.println("i am thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
while(true){
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
回调函数
- 不需要程序员自己主动调用,而是在合适的时机,自动的被调用的函数,是个c语言的东西
经常用到回调函数:
- 服务器开发(服务器收到一个请求就触发一个回调函数)
- 图形界面开发(用户的某个操作,触发一个对应的回调)
4.0 Thread 构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用 Runnable 对象创建线程对象 |
Thread(String name) | 创建线程对象,并命名,不然默认thread-0等,可读性差 |
hread(Runnable target, String name) | 使用 Runnable 对象创建线程对象,并命名 |
【了解】Thread(ThreadGroup group,Runnable target) | 线程可以被用来分组管理,分好的组即为线程组,这个目前我们了解即可 |
5.0 线程命名
public class Demo6 {
public static void main(String[] args) throws InterruptedException {
//lambda表达式实现一个类 实现了runnable接口,由于是lambda实现类,不用返回值,但是如果是lambda实现函数,则一定要返回值
Thread t = new Thread(()->{
while(true){
System.out.println("i am thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"yzyThread");//线程命名
t.start();
while(true){
System.out.println("i am main");
Thread.sleep(5000);
}
}
}
6.0 获取当前线程引用的方法
方法 | 说明 |
---|---|
public static Thread currentThread() | 返回当前线程对象的引用 |
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
7.0 开始和执行线程的方法
start方法
在系统中,真正创建出线程(1.创建出pcb,把pcb加入到对应的链表中)
操作系统=内核(一个系统最核心的功能:对下管理好各种硬件,对上:为各种程序提供稳定的运行环境) + 配套的程序(给用户使用,就像银行你只能去大厅不能去柜台)
应用程序在银行大厅,通过柜台(系统API/系统调用)去创建线程
应用程序,只能在银行大厅行动,不能进入柜台,程序处于"用户态"
应用程序,能进入银行柜台,直接操作系统内核数据结构,程序处于"内核态",这样的程序就是银行内部人员了(驱动程序就是这种该程序)
start只是告诉系统要创建一个线程出来,没有别的操作,调用start完毕之后,代码立即继续执行start的后续逻辑
结束的条件
线程是通过start创建的,一个线程的入口方法执行完毕(对于主线程是main执行完毕,对于其他线程run/lambda 的执行完毕)
8.0 获取 Thread属性 的方法
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同线程不会重复 (JVM给线程的标识),(pthread的库,这个是系统给程序员提供的线程api,也有身份标识),(内核里,针对线程的pcb还有身份标识),这些身份标识都是相互独立的
- 名称是各种调试工具用到
- getState()状态表示线程当前所处的一个情况,与操作系统中的有一定差异
- 优先级高的线程理论上来说更容易被调度到,但线程的调度主要还是系统内核来负责,而且系统调度太快了
- **前台线程:会影响到进程结束,前台线程没结束,进程是不会结束的isDaemon(),关于后台线程/守护线程:**需要记住一点:JVM会在一个进程的所有非后台线程结束后,才会结束运行(即一个进程的所有前台线程都执行完了,退出了,此时即使存在后来线程仍然没执行完,也会随着进程一并退出);
- 通过 线程名字.setDeamon(true),手动将线程设置成后台
- 是否存活,对应的是Thread对象对应的线程(系统内核中)是否存活,Thread对象大的生命周期并不是和系统中的线程完全一致的, 即简单的理解,为 run 方法是否运行结束了 (一般是Thread对象先创建好,手动调用start,内核才真正创建出线程,消亡的时候,可能thread对象还在,内核中线程把run执行完了,线程也结束了,就不存活了)
- 线程的中断问题,下面题
后台线程的结束
//后台线程的结束
public class Demo7 {
public static void main(String[] args) throws InterruptedException {
Thread t= new Thread(()->{
while(true){
System.out.println("i am thread");
}
});
// 线程名字.setDeamon(true),手动将线程设置成后台,见上一个标题的线程属性
t.setDaemon(true);
System.out.println("i am main,i start to sleep here");
System.out.println("after 5 s,i will end");
Thread.sleep(5000);
}
}
9.0 终止(中断)一个线程相关方法
一个线程的run方法执行完毕,就算终止了
此处的终止线程,就是想办法,让run能够尽快的执行完毕
方法1:通过程序员手动设置标志位
//线程终止
public class Demo8 {
public static boolean OK=false;//标志位
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread( ()->{
while(!OK){
System.out.println(" i am thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
//主线程运行一段时间让线程t结束
System.out.println("5s after,end");
Thread.sleep(5000);
//修改标志位,让线程结束
System.out.println("线程终止");
OK=true;
}
}
方法2: 直接Thread类,提供好了线程的标志位
//Thread类自带的线程标志位
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(()->{
//currentThread() 是获取当前线程的引用对象(Thread),但是lambda表达式是在构造t之前就定义好的,应为这里还没有创建 就引用t是非法的,所以只能这干才能获取构造好的t
//Thread 提供了一个静态方法isInterrupted(),在哪个线程调用这个方法,就能获取到哪个线程的标志位,true应该结 束,false则线程先不必结束
while(!Thread.currentThread().isInterrupted()){
System.out.println("i am thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
break;//线程的sleep被interrupt()打断后抛出异常,这里break就医不对异常做操作而继续运行
}
}
});
t.start();
Thread.sleep(5000);
//把上面t的标志位设置成true,相当于唤醒,但是sleep也会涉及到唤醒和睡眠,这就导致可能在sleep的时候被强制唤醒了,手动的唤醒 优先级更高
//线程在sleep的过程中,线程调用interrupt的方法时就会强制使sleep抛出一个异常,sleep就被立即唤醒
//但是sleep在被唤醒的时候,会自动清除标志位(给程序员留下更多操作,没到点该如何让操作,留给程序员),interrupt给标志位设置 成true,但是sleep唤醒后会清除标志位,如果没有break则程序会死循环
t.interrupt();
}
}
中断线程的方法
中断就代表线程要结束了,并且抛异常,让你处理准备结束的线程最后做点什么
方法 | 说明 |
---|---|
public void interrupt() | 中断对象关联的线程,如果线程正在阻塞,则以异常方式通知, 否则设置标志位 |
判定标志位的方法
方法 | 说明 |
---|---|
public static boolean interrupted() | 判断当前线程的中断标志位是否设置,调用后清除标志位 |
public boolean isInterrupted() | 判断对象关联的线程的标志位是否设置,调用后不清除标志位 |
Thread.intertupted();//静态方法判定,不建议使用,会在判断的同时清除标志位
Thread.currentThread().//成员方法,判断标志位,不会清除更推荐使用
sleep被强制唤醒后
当sleep被强制唤醒后可以进行以下操作
- 线程在sleep的过程中,线程调用interrupt的方法时就会强制使sleep抛出一个异常,sleep就被立即唤醒,但是sleep在被唤醒的时候,会自动清除标志位(给程序员留下更多操作,没到点该如何让操作,留给程序员),interrupt给标志位设置成true,但是sleep唤醒后会清除标志位,那么程序员就可以做以下操作
- 立即停止循环,立即结束线程,在catch中直接break
- 先做点事,过一会再结束进程,在catch中执行别的逻辑,执行完后再考虑要不要break
- 忽略终止请求,继续循环,不写break,sleep被强制唤醒,清除了标志位,catch完后则会继续循环不停止
//Thread类自带的线程标志位
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t =new Thread(()->{
//currentThread() 是获取当前线程的引用对象(Thread),但是lambda表达式是在构造t之前就定义好的,应为这里还没有创建 就引用t是非法的,所以只能这干才能获取构造好的t
//Thread 提供了一个静态方法isInterrupted(),在哪个线程调用这个方法,就能获取到哪个线程的标志位,true应该结 束,false则线程先不必结束
while(!Thread.currentThread().isInterrupted()){
System.out.println("i am thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
//e.printStackTrace();
break;//线程的sleep被interrupt()打断后抛出异常,这里break就医不对异常做操作而继续运行
}
}
});
t.start();
Thread.sleep(5000);
//把上面t的标志位设置成true,相当于唤醒,但是sleep也会涉及到唤醒和睡眠,这就导致可能在sleep的时候被强制唤醒了,手动的唤醒 优先级更高
//线程在sleep的过程中,线程调用interrupt的方法时就会强制使sleep抛出一个异常,sleep就被立即唤醒
//但是sleep在被唤醒的时候,会自动清除标志位(给程序员留下更多操作,没到点该如何让操作,留给程序员),interrupt给标志位设置 成true,但是sleep唤醒后会清除标志位,如果没有break则程序会死循环
t.interrupt();
}
}
10.0 等待其他线程的方法
join()方法
概念:
-
多个线程是并发执行的,具体的执行过程,都是由操作系统负责调度的,操作系统的线程调度是"随机的",无法确定执行的先后
-
上述的随机性,程序员不太喜欢,更喜欢确定的东西,就有了等待线程这一概念,这是一种规划线程结束的顺序手段
-
A,B两个线程,希望B先结束,A后结束,此时就可以让A线程调用B.join的方法,此时B线程还没执行完,但A执行完了,A线程就会进入阻塞状态,相当于给B留下执行时间,等B的run方法执行完毕,A再从阻塞状态恢复回来,并且继续执行
阻塞
-
让代码暂时不继续执行了(该线程暂时不去cpu上参与调度)
-
join的阻塞是死等
join的几种写法
方法 | 说明 |
---|---|
public void join() //死等 | 等待线程结束 |
public void join(long millis) | 等待线程结束,最多等 millis 毫秒 |
public void join(long millis, int nanos) | 同理,但可以更高精度 |
唤醒
sleep,join.wait 产生阻塞之后都是可以被interrupt的方法唤醒的,这几种方法都会再被唤醒的时候自动清除标志位 (和sleep类似的,并且抛出异常,可以在被唤醒后做出部分操作)
代码测试
//测试等待join
public class Demo10 {
private static int i;
public static void main(String[] args) {
Thread b = new Thread(()->{
for(int i=0;i<7;i++){
System.out.println("i am thread B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("b end ~");
});
Thread a = new Thread(()->{
for(int i=0;i<3;i++){
System.out.println("i am thread a");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
//到这里如果b还没执行完毕,则b.join就会让A线程在这个地方阻塞,从而等待b执行完毕
b.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("a end ~");
});
b.start();
a.start();
}
}
yield() 方法
yield() 大公无私,让出 CPU
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("张三");
Thread.yield();
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println("李四");
}
}
}, "t2");
t2.start();
可以看到:
- 不使用 yield 的时候, 张三李四大概五五开
- 使用 yield 时, 张三的数量远远少于李四
结论:
yield 不改变线程的状态, 但是会重新去排队
11.0 休眠当前线程的方法
sleep() 方法
方法 | 说明 |
---|---|
public static void sleep(long millis) throws InterruptedException | 休眠当前线程 millis 毫秒 |
休眠当前线程 millis 毫秒 | 可以更高精度的休眠 |
异常问题
-
如果像实现runnable接口但是run函数又没有throws异常的,则要在run内写明try,catch
-
但是如果像main可以手动throws则不需要再try,catch了
-
sleep本生存在误差,应为线程调度也是需要时间的
-
sleep(1000); 就是 1000ms后就恢复成就绪状态,此时可以随时去cpu上执行,但不一定是立马就去指定
-
因为sleep的特性,也诞生了一个特殊的技巧 sleep(0) ,这个写法的意思是让当前线程放弃cpu让别人去执行,他会让当前线程让别的线程调度,而这个线程加入到等待队列,等待下一轮调度(嵌入式开发经常用)
-
yield 的方法和sleep(0)效果一样,yield 不改变线程的状态, 但是会重新去排队.
12.0 volatile关键字
内存可见性问题(线程安全)
例子
JVM对汇编做了优化,比如一个线程A 1s中判定1万次 OK变量是否为0,又不做任何操作,JVM就把load(从内存中读指令,很慢)去掉,只剩下jcmp(判断跳转指令(在寄存器中很快)),但是别的线程又对内存的OK变量做了更改,但是原线程A没有访问主存,因此不知道修改了OK变量,只有jcmp判断则造成死循环,jvm优化效率,多线程编程,java内存模型共同导致的bug
用volatile 来修饰一个变量, 编译器就会明白,这个变量是"易变"的,编译器就禁止按照上述jvm的优化方式把load操作给优化掉,就能保证上诉例子中的线程A能一直读取到主存
volatile 本质保证变量的内存课件性(禁止该变量的读操作被优化到寄存器中,防止别的线程修改这个寄存器上的相关变量并载入主存)
volatile保证内存可见性,不是原子性
类属性,类方法 就是static修饰的, 实例属性,实例方法就是new一个类的new出来的(但c中static又有外的用途)
例子
//测试volatile
public class Ddemo13 {
private volatile static int OK=0;//volatile修饰,OK变量内存可见
public static void main(String[] args) {
Thread t1 = new Thread(()->{
while(OK==0){
;
}
System.out.println("t1 执行完毕. ");
});
Thread t2 = new Thread(()->{
Scanner scanner = new Scanner(System.in);
System.out.println("输入非0 t1结束, 输入0t1 循环: ");
OK=scanner.nextInt();
});
t1.start();
t2.start();
}
}
指令重排序
编译器会自动优化一段代码,保证代码在原有的逻辑不变的前提下,对代码顺序进行调整,使调整之后的执行效率提升
例子代码:
public synchronized static SingletonLazy getInstance() {
if(instance == null){
synchronized (locker){
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
问题:
-
jvm编译为了加快速度可能指令重排序,比如把
instance = new SingletonLazy();
这个代码的中的三个原子步骤 1.为对象创建出内存空间,2.在空间上调用构造方法,3.对对象进行初始化 调换顺序 -
由于 synchronized打包的下半部分,则可能导致另一个B线程也能进到这个函数并且判断
instance == null
,由于调换顺序导致在A线程中先开辟空间,但没经过构造直接把空间赋值给对象从而造成另一个线程直接返回instance,而这个sinsatnce是不完整的
解决办法:
- 给instance 加上 volatile之后,此时争对 instance 进行的赋值操作,就不会产生上述的指令重排序问题,会严格按照1.为对象创建出内存空间,2.在空间上调用构造方法,3.对对象进行初始化执行
解决后的代码:
//懒汉模式(多线程)
class SingletonLazy {
//最开始为空
private static volatile SingletonLazy instance = null;
//创建一个锁对象
private static Object locker =new Object();
//把构造方法设置成私有,避免他人new这个对象
private SingletonLazy() {}
//首次运行getInstance才会创建对象
public synchronized static SingletonLazy getInstance() {
// synchronized相当于就是把下面这一段指令打包,这样就变成原子性的了
//也可以写成 synchronized (SingletonLazy.class) , 把这个类对象当锁对象(只要是用这个类创建的对象调用及锁)
//第一个if是判断是否要加锁,减少了多次阻塞的问题.第二个if是判断是否要创建对象
if(instance == null){
synchronized (locker){
if (instance == null) {
instance = new SingletonLazy();//new这个指令是原子的
}
}
}
return instance;
}
}
13.0 线程主动阻塞的方法
wait
两个关键字用来协调线程顺序的重要工具 wait等待,notify 唤醒
wait 和notify 都是Object类的方法,而join是Thread的方法
当wait引起线程阻塞后,使用interrupt/notify方法,可以把线程唤醒,打断当前的阻塞
在java中 synchronized 叫做 监视器锁(Monitor)
java标准库建议使用wait的时候搭配while循环使用
wait做的事情:
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 释放当前对象的锁 (但是前提能wait的对象一定要先有锁,所以做法是先加锁,在synchronized里面再进行wait,wait本质是释放锁)
- 满足一定条件时被唤醒, 重新尝试获取这个锁(当被其他线程唤醒后,就会尝试重新加锁)
wait结束等待的条件
- 其他线程调用该对象的 notify 方法.
- wait 等待时间超时 (wait 方法提供一个带有 timeout 参数的版本, 来指定等待时间).
- 其他线程调用该等待线程的 interrupted 方法, 导致 wait 抛出
InterruptedException
异常
wait/notify作用
- wait和notify 主要用途是安排线程之间的执行顺序,其中一个最典型的场景就是能够避免"线程饿死/饥饿(吃不到cpu资源)"
关键字
wait() / wait(long timeout) //让当前线程进入等待状态
代码测试
//测试wait 和 notify
public class Demo15 {
//使用这个锁对象来负责作为wait和notify的加锁解锁对象
private static Object locker = new Object();
public static void main(String[] args) {
//线程1
Thread t1 = new Thread(()->{
while(true) {
//内部类调用外部类
synchronized (locker) {
System.out.println("t1 开始 进行 wait");
try {
locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 线程wait结束");//wait会让t1卡在wait那一步,直到notify才会执行这个打印语句
}
}
});
t1.start();
//线程2
Thread t2 = new Thread(()->{
//让notify发生的时间晚于wait()
while(true){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker){
System.out.println("t2 notify 开始");
locker.notify();
System.out.println("t2 notify 结束");
}
}
});
t2.start();
}
}
notify
**notify 方法是唤醒等待的线程. **
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的
- 其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”) - 在notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
小区别
- 想要notify能够唤醒wait就需要保证wait和notify都是使用同一个对象调用
- wait和notify都需要放到synchronized之内,虽然notify不涉及"解锁操作",但java 也强要求放到synchronized中(系统原生api中没这个要求)
- 如果进行notify的时候,另一个线程没有处于wait状态,此时notify相当于空打一炮,但不会有任何操作
notifyAll()方法
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程(同一个类)
wait和sleep区别
- wait 需要搭配 synchronized 使用. sleep 不需要.
- wait 是 Object 的方法 sleep 是 Thread 的静态方法.
两种唤醒notify和interrput的区别
notify 和 interrupt 都能唤醒wait()
但是notify唤醒后线程后线程正常继续工作
interrupt 唤醒线程后,线程将要结束,接下来线程进入到尾声,通过try-catch来表明结束前要做些什么
因此考虑协调多个线程之间的执行顺序,还是有限考虑wait和noticfy