请参看前一篇文章: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 的使用权,等待再次被调度。
不同点:
- wait 属于 Object 的方法,sleep 是 Thread 的方法
- wait 只能在同步方法或同步块中调用,sleep 可以在任何地方调用
- wait 会释放线程持有的锁,sleep 不会释放锁资源
七、管程理论
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 种状态,分别是:
- NEW(初始化状态)
- RUNNABLE(可运行/运行状态)
- BLOCKED(阻塞状态)
- WAITING(无限时等待)
- TIMED_WAITING(限时等待)
- TERMINATED(终止状态)
2. 线程状态转换
- RUNNABLE 与 BLOCKED 状态的转换:在线程等待 synchronized 的锁时,会进入 BLOCKED 状态,当获取到锁之后,又转换到 RUNNABLE 状态。
- RUNNABLE 与 WAITING 状态的转换:1) 线程获取到 synchronized 锁之后,并且调用了 wait() 方法。 2) 调用 Thread.join() 方法,例如线程 A 调用 join() 方法,线程 B 等待 A 执行完毕,等待期间 B 进入 WAITING 状态,线程 A 执行完后,线程 B 切换到 RUNNABLE 状态。3) 调用 LockSupport.park() 方法
- RUNNABLE 与 TIMED_WAITING 状态的转换:以上三种情况,分别在方法中加上超时参数即可。另外还有两种情况:Thread.sleep(long millis) 方法,LockSupprt.parkNanos(Object blocker, long deadline)。
- NEW 到 RUNNABLE 状态的转换:在 Java 中新创建的线程,会立即进入 NEW 状态,然后启动线程进入 RUNNABLE 状态。Java 中新建线程一般有三种方式:
- 继承 Thread 类
public class MyThread extends Thread { @Override public void run() { System.out.println("I am roseduan"); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } • 实现 Runnable 接口,并将其实现类传给 Thread 作为参数 public class MyThread { public static void main(String[] args) { Thread thread = new Thread(new Print()); thread.start(); } } class Print implements Runnable{ @Override public void run() { System.out.println("I am roseduan"); } } • 实现 Collable 接口,将其实现类传给线程池执行,并且可以获取返回结果 public class ThreadTest { public static void main(String[] args) throws InterruptedException { //线程池 BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(5); ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 1, TimeUnit.HOURS, queue); //执行 Future<?> submit = threadPool.submit(new Demo()); } } class Demo implements Callable<String> { @Override public String call() { System.out.println("I am roseduan"); return "I am roseduan"; } }
- NEW 到 TERMINATED 状态的转换:线程执行完 run() 方法后,会自动切换到 TERMINATED 状态。如果手动中止线程,可以使用 interrupt() 方法。
3. 局部变量的线程安全性
局部变量存在于方法中,每个方法都有对应的调用栈帧,由于每个线程都有自己独立的方法调用栈,因此局部变量并没有被共享。所以即便多个线程同时调用同一个方法,方法内部的局部变量也是线程安全的,不需要单独加锁。