JavaEE初阶 - 多线程基础篇 (死锁, 可重入锁, 线程通知和等待)

1. 死锁和可重入锁
2. 死锁的必要条件
3. Java标准库中有关线程安全的类
4. 线程通知和等待


死锁和可重入锁

  同一个线程针对同一个锁, 连续加锁两次, 如果出现死锁, 那就是不可重入锁, 没有出现死锁, 就是可重入锁.

连续加锁两次:

synchronized public void func(){
    synchronized (this){
        System.out.println("连续加锁两次");
    }
}

上述代码中, 外层加了一层锁之后, 里层又对同一个对象加了一层锁.

外层锁:进入这个方法前, 这个方法里的语句并未加锁, 此时进入方法即可进行加锁
里层锁:进入代码块前, 这里面的内容已经被外层加锁了, 因此不能加锁成功

  里层想要进行加锁, 需要外层将锁解除, 怎么解除呢? 将外层的方法执行完, 但外层方法要想执行完就需要里层进行加锁, 而里层进行加锁就需要外层先将方法执行完, 这样就会陷入死循环, 这就是死锁.

  要想解决死锁的问题, 就要实现可重入锁, 可重入锁会记录当前的锁被哪个线程占用着, 同时会记录一个加锁次数, 当线程A第一次加锁时, 锁的内部就记录了当前进行加锁的是A, 同时加锁次数记为1, 后续A再进行加锁时, 此时并不会真正地加锁, 而是仅仅将加锁次数加一, 后续解锁时, 每次将计数器减一, 直到等于0, 才能真正解锁.

可重入锁的意义就是提高了开发效率, 但同时也为程序带来了更高的开销, 降低了运行效率.

死锁的必要条件

  1. 互斥使用: 一个锁被一个线程占用之后, 其他线程不能占用

  2. 不可抢占: 一个锁被一个线程占用之后, 其他线程不能将锁抢占

  3. 请求和保持:当一个线程占据多把锁后, 除非显式地释放锁, 否则该线程始终持有这些锁

  4. 环路等待: 等待关系形成了环路, 如: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方法时:

  1. wait()方法会使当前的线程进行等待, 将线程放入等待队列中
  2. 释放当前的锁(上面提到过了, wait只能在加锁的线程中使用)
  3. 当其他线程唤醒这个线程时, 这个线程会重新尝试获取这个锁.

  如果有10个线程都对同一个对象执行了wait()操作, 此时这10个线程都处于阻塞状态, 当调用notify()方法时, 只会随机地唤醒一个线程, 而如果使用notifyall()方法, 这10个线程都会被唤醒. 这10个线程都被唤醒后, 就会重新尝试获取到锁, 这个过程又会发生竞争, 因此, notifyall()这个方法并不常用, 我们一般更倾向于使用notify()方法.


The end

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhanglf6699

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值