1.死锁
1.1产生死锁四个必要条件
只要破坏其中一条就能打破死锁
1)互斥条件:锁是互斥的,同一时间只能一个线程拿到锁。
2)请求与保持:线程拿到一把锁,又申请另一把锁,并且不释放锁。
3)不可剥夺:锁只能自己主动释放,别的不可抢夺。
4)循环等待:线程a拿到锁1,申请拿锁2。线程2拿到锁2,申请拿锁1。形成了循环。
上述代码执行过程
首先t1先拿到锁lock1,并且进入睡眠,不释放lock1。
然后t2拿到锁lock2,也进入睡眠,不释放lock2。
当t1苏醒后在申请拿lock2,此时锁lock2已经被占用,会陷入堵塞。
当t2苏醒后在申请拿lock1,此时锁lock1已经被占用,会陷入堵塞。
此时陷入循环
输出结果如下,程序卡住不会结束
解决方法:我们可以对锁进行编号,任何一个线程在有多把锁的时候,加锁顺序由小到大加锁
此时输出可达到预期
2.volatile 关键字
volatile用来解决由于内存可见性引起的线程安全问题
什么是内存可见性问题?
实例:
在多线程中,每个线程有着自己的工作内存,不是直接读写主内存(存共享变量)
线程a修改了变量,但是只改在了自己的缓存里,没有同步到主内存中,线程b再从主内存中读取到的数据还是原来的
比如下述例子
解决上述方法就是给变量加上volatile 关键字
private staticvolatileboolean flag = false;
再加上后,修改flag值会立刻把修改后的值放回到主内存中并且清空其他线程的缓存副本
在读volatile变量时直接从主内存中读,不会从线程自己的工作缓存中读
保证了一个线程修改 volatile 变量,其他线程马上能读到最新值。
volatile三个作用
1)保证内存的可见性
2)禁止指令重排序
CPU / JVM 为了提升运行效率,在不改变单线程执行结果的前提下,会打乱代码执行顺序,这就叫指令重排序。
3)不保证原子性
3.wait 和notify
3.1wait()方法
wait()会使当前执行代码的线程进入等待队,并且会释放锁,等待唤醒
唤醒条件:其他线程调⽤notify⽅法。
wait(long millis)可以设置最大等待时间,时间到自动唤醒,不等 notify。
3.2notify()⽅法
notify()会随机唤醒等待队列中的线程
在notify()⽅法后,当前线程不会⻢上释放该对象锁,要等到执⾏notify()⽅法的线程将程序执⾏ 完,也就是退出同步代码块之后才会释放对象锁
notifyAll():唤醒该对象等待池全部线程。
3.3代码演示
注意:必须在synchronized(obj){ }内部调用,否则直接抛异异常
执行结果
3.4wait 和sleep的对⽐
1)锁行为:
wait():释放锁,别的线程可以拿到这把锁。
sleep():抱着锁睡觉,如果在同步块里 sleep,其他线程拿不到锁,全部阻塞。
2)所属类
wait是Object实例对象的方法:lock.wait()
sleep是线程静态方法:Thread.sleep(1000)
3)唤醒区别
wait:要别的线程调用 notify /notifyAll 才会被唤醒;也可以被中断
sleep:时间到自己醒,不需要别的线程通知
3)位置
wait需要搭配synchronized使⽤.sleep不需要