1. 死锁和可重入锁
2. 死锁的必要条件
3. Java标准库中有关线程安全的类
4. 线程通知和等待
死锁和可重入锁
同一个线程针对同一个锁, 连续加锁两次, 如果出现死锁, 那就是不可重入锁, 没有出现死锁, 就是可重入锁.
连续加锁两次:
synchronized public void func(){
synchronized (this){
System.out.println("连续加锁两次");
}
}
上述代码中, 外层加了一层锁之后, 里层又对同一个对象加了一层锁.
外层锁:进入这个方法前, 这个方法里的语句并未加锁, 此时进入方法即可进行加锁
里层锁:进入代码块前, 这里面的内容已经被外层加锁了, 因此不能加锁成功
里层想要进行加锁, 需要外层将锁解除, 怎么解除呢? 将外层的方法执行完, 但外层方法要想执行完就需要里层进行加锁, 而里层进行加锁就需要外层先将方法执行完, 这样就会陷入死循环, 这就是死锁.
要想解决死锁的问题, 就要实现可重入锁, 可重入锁会记录当前的锁被哪个线程占用着, 同时会记录一个加锁次数, 当线程A第一次加锁时, 锁的内部就记录了当前进行加锁的是A, 同时加锁次数记为1, 后续A再进行加锁时, 此时并不会真正地加锁, 而是仅仅将加锁次数加一, 后续解锁时, 每次将计数器减一, 直到等于0, 才能真正解锁.
可重入锁的意义就是提高了开发效率, 但同时也为程序带来了更高的开销, 降低了运行效率.
死锁的必要条件
-
互斥使用: 一个锁被一个线程占用之后, 其他线程不能占用
-
不可抢占: 一个锁被一个线程占用之后, 其他线程不能将锁抢占
-
请求和保持:当一个线程占据多把锁后, 除非显式地释放锁, 否则该线程始终持有这些锁
-
环路等待: 等待关系形成了环路, 如:A等B, B等A, 或者A等B, B等C, C等A
前三个条件都属于锁本身的特点, 要想避免产生死锁, 我们就需要避免出现环路等待, 解决方式就是, 针对多把锁加锁时, 提前约定固定的顺序, 只要所有的线程都遵守同样的顺序, 就不会产生环路等待.
Java标准库中有关线程安全的类
Java标准库中, 有很多类是线程不安全的, 这些类中并没有synchronized关键字修饰的方法:
ArrayList, LinkedList, HashMap, TreeMap, HashSet, TreeSet, StringBuilder, 这些类都是线程不安全的.
而像ConcurrentHashMap, StringBuffer, 这些类中使用了锁机制来控制线程安全, 这些类都是线程安全的. 除此之外, 还有一个特殊的类, String类. 这个类内部并没有进行加锁, 但Sting类的内部并没有提供修改操作, 因此String类也是线程安全的.
线程通知和等待
我们可以通过wait()方法和notify()方法来实现线程的通知和等待.
wait()和notify()都是Object对象的方法, 某个线程调用了wait方法, 就会进行等待, 直到有其他的线程使用notify()通知(wait和notify必须搭配synchronized使用).
//创建一个Object类的对象, 使用wait()方法
public static void main(String[] args) throws InterruptedException{
Object object = new Object();
//注意:wait()一定要搭配synchronized使用, 否则程序会抛出异常
synchronized(object){
System.out.println("wait前");
object.wait();
System.out.println("wait后");
}
}
//结果:
wait前
//object对象持续进行等待, 程序未结束
我们可以使用wait和notify来协调线程之间的执行顺序.
public class Demo {
public static Object object = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() ->{
//执行wait
synchronized(object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("后执行的操作");
}
});
Thread thread2 = new Thread(() ->{
//执行notify
synchronized(object){
System.out.println("先执行的操作");
object.notify();
}
});
thread1.start();
thread2.start();
}
}
//结果:
先执行的操作
后执行的操作
除此之外, 还有一个线程通知的方法, notifyall(), 很显然, 这个方法是唤醒所有调用了wait()方法的线程.
首先, 我们需要了解wait()方法的执行过程. 当我们使用wait方法时:
- wait()方法会使当前的线程进行等待, 将线程放入等待队列中
- 释放当前的锁(上面提到过了, wait只能在加锁的线程中使用)
- 当其他线程唤醒这个线程时, 这个线程会重新尝试获取这个锁.
如果有10个线程都对同一个对象执行了wait()操作, 此时这10个线程都处于阻塞状态, 当调用notify()方法时, 只会随机地唤醒一个线程, 而如果使用notifyall()方法, 这10个线程都会被唤醒. 这10个线程都被唤醒后, 就会重新尝试获取到锁, 这个过程又会发生竞争, 因此, notifyall()这个方法并不常用, 我们一般更倾向于使用notify()方法.
The end