Java 杂谈程序员技术栈

Java 并发学习笔记(二)

2019-05-24  本文已影响1人  roseduan写字的地方

以下内容接前一篇文章:

Java 并发学习笔记(一)——原子性、可见性、有序性问题

六、等待—通知机制

什么是等待通知—机制?当线程不满足某个条件,则进入等待状态;如果线程满足要求的某个条件后,则通知等待的线程重新执行。

等待通知机制的流程一般是这样的:线程首先获取互斥锁,当不满足某个条件的时候,释放互斥锁,并进入这个条件的等待队列;一直等到满足了这个条件之后,通知等待的线程,并且需要重新获取互斥锁。

1. 等待-通知机制的简单实现

等待-通知机制可以使用 Java 的 synchronized 关键字,配合 wait()、notify()、notifyAll() 这个三个方法来实现。

前面说到的解决死锁问题的那个例子,一次性申请所有的资源,使用的是循环等待,这在并发量很大的时候比较消耗 CPU 资源。

现在使用等待-通知机制进行优化:

final class Monitor {
    
    private List<Object> res = new ArrayList<>(2);
    
    /**
     * 一次性申请资源 
     */
    public synchronized void apply(Object resource1, Object resource2) {
        while (res.contains(resource1) || res.contains(resource2)){
            try {
                //条件不满足则进入等待队列
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        res.add(resource1);
        res.add(resource2);
    }
    /** 
     * 归还资源 
     */
    public synchronized void free(Object resource1, Object resource2){
        res.remove(resource1);
        res.remove(resource2);
        //释放资源之后,通知等待的线程开始执行
        this.notifyAll();
    }
}
2. 需要注意的地方
  1. 每个互斥锁都有相应的等待队列,例如上面的例子,就存在两个等待队列,一是 synchronized 入口等待队列,二是 while 循环这个条件的等待队列。

  2. 调用 wait() 方法,会使当前线程释放持有的锁,并进入这个条件的等待队列。满足条件之后,队列中的线程被唤醒,不是马上执行,而是需要重新获取互斥锁。例如上图中,if 条件的队列中的线程被唤醒后,需要重新进入 synchronized 处获取互斥锁。

3. wait 和 sleep 的区别

相同点:两个方法都会让渡 CPU 的使用权,等待再次被调度。

不同点:

七、管程理论

1. 什么是管程?

指的是对共享变量和对共享变量的操作的管理,使其支持并发,对应到 Java,指的是管理类的成员变量和方法,让这个类是线程安全的。

2. 管程模型

管程主要的模型有 Hasen、Hoare、MESA ,其中 MESA 最常用。管程的 MESA 模型主要解决的是线程的互斥和同步问题,和上面说到的等待-通知机制十分类似。示意图如下:

在这里插入图片描述

首先看看管程是如何实现互斥的?在管程的入口有一个等待队列,一次只允许一个线程进入管程。每个条件对应一个等待队列,当线程不满足条件的时候,进入对应的等待队列;当条件满足的时候,队列中的线程被唤醒,重新进入到入口处的等待队列获取互斥锁,这就实现了线程的同步问题。

3. 管程的最佳实践

接下来使用代码实现了一个简单的阻塞队列,这就是一个很典型的管程模型,解决了线程互斥和同步问题。

public class BlockingQueue<T> {
    private int capacity;
    private int size;

    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();

    /**
     * 入队列
     */
    public void enqueue(T data){
        lock.lock();
        try {
            //如果队列满了,需要等待,直到队列不满
            while (size >= capacity){
                notFull.await();
            }
            //入队代码,省略
            //入队之后,通知队列已经不为空了
            notEmpty.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    /**
     * 出队列
     */
    public T dequeue(){
        lock.lock();
        try {
            //如果队列为空,需要等待,直到队列不为空
            while (size <= 0){
                notEmpty.await();
            }
            //出队代码,省略
            //出队列之后,通知队列已经不满了
            notFull.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
        //实际应该返回出队数据
        return null;
    }
}

八、Java 中的线程

1. 线程的生命周期

Java 中的线程共分为了 6 种状态,分别是:

2. 线程状态转换
3. 局部变量的线程安全性

局部变量存在于方法中,每个方法都有对应的调用栈帧,由于每个线程都有自己独立的方法调用栈,因此局部变量并没有被共享。所以即便多个线程同时调用同一个方法,方法内部的局部变量也是线程安全的,不需要单独加锁。

经极客时间《Java 并发编程实战》专栏内容学习整理

上一篇下一篇

猜你喜欢

热点阅读