简明易懂多线程(四) JAVA

多线程(一)
多线程(二)
多线程(三)

死锁

可重入性

首先我们先回忆一下什么是可重入性

  • 同一个线程可以多次获取同一个锁
  • 同一个线程可以获取多个不同的锁

因此获取锁时不但会判断是否是第一次获取,还要记录这是第几次获取。
每获取一次,记录+1,每退出被锁住的代码块一次,记录-1,当记录减到0时锁才真正的释放

死锁

当两个线程各自持有不同的锁,然后各自试图获取对方手里的锁,造成双方无线等待,这就是死锁

下面我们看个例子

public void add(int m) {
    synchronized(lockA) { // 获得lockA的锁
        this.value += m;
        synchronized(lockB) { // 获得lockB的锁
            this.another += m;
        } // 释放lockB的锁
    } // 释放lockA的锁
}

public void dec(int m) {
    synchronized(lockB) { // 获得lockB的锁
        this.another -= m;
        synchronized(lockA) { // 获得lockA的锁
            this.value -= m;
        } // 释放lockA的锁
    } // 释放lockB的锁
}

当线程1、2分别执行add()和dec()时,如果

  • 线程1: 进入add(),获取锁A
  • 线程2: 进入dec(),获取锁B
    随后
  • 线程1: 准备获得锁B,失败,进入等待
  • 线程2: 准备获取锁A,失败,进入等待

此时线程1,线程2都陷入了无限的等待中,也就造成了死锁。
死锁发生后,一般只能强制结束JVM线程。

避免死锁的发生

在编写多线程应用时,尽量让获取锁的顺序一致。

wait和notify

前面的synchronized解决了多线程竞争的问题,但是synchronized并没有解决多线程协调的问题。以下面的程序为例

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
    }

    public synchronized String getTask() {
        while (queue.isEmpty()) {
        }
        return queue.remove();
    }
}

看上去这个代码没有问题,getTask()先判断队列是否为空,如果为空则循环等待,直到另一个线程中使用了addTask()添加了元素,然后就可以正常获取队列中的元素。

但是事实上while循环永远都不会退出,因为线程在执行while循环时已经在getTask()获取了this的锁,其他线程因为没法获得this锁,根本无法调用addTask()方法。因此while循环会变成死循环。

我们想要实现的效果是:

  • 线程1可以调用addTask()方法不断往队列中添加元素
  • 线程2可以调用getTask()方法获取队列中的元素,如果队列为空,则应该等待,直至队列中有一个元素在返回

由此可以看出,多线程协调运行的原则:当条件不满足时线程进入等待;当条件满足时,线程被唤醒,继续执行

通过wait()方法实现条件不满足时等待:

public synchronized String getTask() {
    while (queue.isEmpty()) {
        this.wait();
    }
    return queue.remove();
}

当执行到while循环中的wait()方法时,wait()不会返回且线程将等待状态,直到被其他进程唤醒wait()才会返回,然后继续执行下一条语句。

wait()方法的执行机制非常复杂,我们只需大概了解。首先,它不是一个普通的Java方法,而是定义在Object类的一个native方法,也就是由JVM的C代码实现的。其次,必须在synchronized块中才能调用wait()方法,因为wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁

需要注意的是只能在锁对象上调用wait()方法
getTask()的锁对象是this,所以在this上调用wait()

通过notify()实现条件满足时唤醒线程:

public synchronized void addTask(String s) {
    this.queue.add(s);
    this.notify(); // 唤醒在this锁等待的线程
}

notify()方法会唤醒一个正在this锁等待的线程,从而使得等待的线程的wait()方法返回

接下来我们看一个完整的例子

public class Main {
    public static void main(String[] args) throws InterruptedException {
        var q = new TaskQueue();
        var ts = new ArrayList<Thread>();
        for (int i=0; i<5; i++) {
            var t = new Thread() {
                public void run() {
                    // 执行task:
                    while (true) {
                        try {
                            String s = q.getTask();
                            System.out.println("execute task: " + s);
                        } catch (InterruptedException e) {
                            return;
                        }
                    }
                }
            };
            t.start();
            ts.add(t);
        }
        var add = new Thread(() -> {
            for (int i=0; i<10; i++) {
                // 放入task:
                String s = "t-" + Math.random();
                System.out.println("add task: " + s);
                q.addTask(s);
                try { Thread.sleep(100); } catch(InterruptedException e) {}
            }
        });
        add.start();
        add.join();
        Thread.sleep(100);
        for (var t : ts) {
            t.interrupt();
        }
    }
}

class TaskQueue {
    Queue<String> queue = new LinkedList<>();

    public synchronized void addTask(String s) {
        this.queue.add(s);
        this.notifyAll();
    }

    public synchronized String getTask() throws InterruptedException {
        while (queue.isEmpty()) {
            this.wait();
        }
        return queue.remove();
    }
}

这个例子中,addTask()方法,内部调用了notifyAll()方法而不是notify(),使用notifyAll()将唤醒所有正在当前this锁等待的线程,而notify()只会唤醒一个(具体哪个依赖操作系统)。这是因为可能有多个线程在getTask()内部等待。通常来说使用notifyAll()更安全,因为如果我们的代码考虑不全,使用notify()很有可能导致一些线程一睡不醒。

特别需要注意的是,wait()方法返回时需要重新获得锁,所以必须等待唤醒这个线程的线程结束,且即使使用notifyAll()一次也只能有一个线程能获得锁,其余继续等待

另外在上述例子中,wait()方法只能用在while循环中,如果是if判断,wait()返回后会直接进入下一条语句,但此时队列仍有可能为空.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值