前言
前面我们说到了死锁以及线程可见性的问题
我们将线程可见性主要归结于是JVM自身的一个bug
一个线程写一个线程读
会将一直不变的变量优化到直接从寄存器中读取,而不是缓存等读取,因为这样我们就设置了使用volatile关键字使得用到这个变量的时候必须从内存中读取数据
死锁主要是四个原因导致:不可抢占,互斥使用,循环等待,请求保持
其中只有循环等待是最好破坏的,我们可以使用规定线程的加锁顺序来破坏这种循环等待的效果
本节我们将讨论wait和notify两个方法的使用
为什么引入这两个方法,有什么用?
和join方法类似,这两个方法还是用来在应用层面上规定代码的执行顺序,事实上在操作系统内核中线程的调度仍然是无序的
这里的干预其实就是让某个线程主动放弃了去cpu执行的权利,相当于放弃了被调度的机会
举个例子:
此时a线程想去atm中取钱,b线程和c线程想进去存钱,恰好此时a拿到了锁,进去了atm房间
此时a就可以一直占用atm机器等待,加入发现没钱出来,a仍然在行列中竞争锁,所以a一直能持有锁(概率问题,就像我跟你分手了,我们复合的概率更大一样),此情况就称之为线程饿死,因为其他的线程都拿不到锁,执行不了自己的代码
(就是某个线程一直反复获取锁,但是又不执行实质性的逻辑)
wait做了什么??
wait方法实际上是做了三件事情
1.释放锁
2.阻塞等待
3.当其他线程调用notify方法的时候,解除阻塞状态,持有锁之后继续运行代码
join和notify的区别
join方法是指假如在主线程中调用t1.join
此时主线程就会等t1线程执行完才会继续执行
而wait方法除非有线程去唤醒他,他会一直等到枯树开出花
当然,我们也是可以设置wait的最大时间的,也就是等不到结果就直接不等了
产生阻塞的几种原因
join/wait BLOCKED
sleep TIMED_WAITING
synchronized BLOCKED
由于wait是object类中的一个方法,所以随便拿个对象都可以使用wait方法,但是得持有锁才行,不然会发生异常
我们一般在synchronized代码块中使用
注:调用wait的对象一般和synchronized的锁一致
一个简单的使用案例
public static void main(String[] args) {//统一对象进行加锁解锁Object lock = new Object();Thread t1 = new Thread(()->{synchronized (lock){System.out.println("t1 wait 之前");try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t1 wait 之后");}});Thread t2 = new Thread(()->{synchronized (lock){try {Thread.sleep(5000);System.out.println("notify之前");lock.notify();System.out.println("notify之后");} catch (InterruptedException e) {throw new RuntimeException(e);}}});t1.start();t2.start();}
此时的结果就是,t1线程在执行过打印效果后,会进入阻塞状态,然后t2睡眠5000ms之后会执行一次打印动作,然后让t1解除阻塞状态,最后执行完下一次打印后释放锁
注:notify方法只会唤醒阻塞的线程,并不会释放锁
注:wait和notify方法是成套使用的,两者依靠对象联系起来
假如这里我们使用object1对象来wait这个线程
再使用objcet2是唤不醒这个线程的
多线程知识点小结
1.线程的特性,线程和进程的区别
2.Thread类创建线程的几种方式
3.Thread类的一些属性
4.启动线程
5.终止线程
6.等待线程
7.线程休眠
8.获取线程引用
9.线程状态
10.线程安全问题
10.1 线程安全产生的原因及解决方法
10.2 死锁问题
10.3 内存可见性问题
11.线程的wait和notify...