2020年1月

阻塞是什么?

故事:老王烧开水。

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

老王想了想,有好几种等待方式

1.老王用水壶煮水,并且站在那里,不管水开没开,每隔一定时间看看水开了没。-同步阻塞

老王想了想,这种方法不够聪明。

2.老王还是用水壶煮水,不再傻傻的站在那里看水开,跑去寝室上网,但是还是会每隔一段时间过来看看水开了没有,水没有开就走人。-同步非阻塞

老王想了想,现在的方法聪明了些,但是还是不够好。

3.老王这次使用高大上的响水壶来煮水,站在那里,但是不会再每隔一段时间去看水开,而是等水开了,水壶会自动的通知他。-异步阻塞

老王想了想,不会呀,既然水壶可以通知我,那我为什么还要傻傻的站在那里等呢,嗯,得换个方法。

4.老王还是使用响水壶煮水,跑到客厅上网去,等着响水壶自己把水煮熟了以后通知他。-异步非阻塞

老王豁然,这下感觉轻松了很多。

通用的线程生命周期

通用线程状态转换图——五态模型
这“五态模型”的详细情况如下所示。

  1. 初始状态,指的是线程已经被创建,但是还不允许分配 CPU 执行。这个状态属于编程语言特有的,不过这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
  2. 可运行状态,指的是线程可以分配 CPU 执行。在这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配 CPU 执行。
  3. 当有空闲的 CPU 时,操作系统会将其分配给一个处于可运行状态的线程,被分配到 CPU 的线程的状态就转换成了运行状态。运行状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,
  4. 休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。线程执行完或者出现异常就会进入终止状态,
  5. 终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。

这五种状态在不同编程语言里会有简化合并。例如,C 语言的 POSIX Threads 规范,就把初始状态和可运行状态合并了;Java 语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而 JVM 层面不关心这两个状态,因为 JVM 把线程调度交给操作系统处理了。

Java 中线程的生命周期

介绍完通用的线程生命周期模型,想必你已经对线程的“生老病死”有了一个大致的了解。那接下来我们就来详细看看 Java 语言里的线程生命周期是什么样的。Java 语言中线程共有六种状态,分别是:NEW(初始化状态)RUNNABLE(可运行 / 运行状态)BLOCKED(阻塞状态)WAITING(无时限等待)TIMED_WAITING(有时限等待)TERMINATED(终止状态)这看上去挺复杂的,状态类型也比较多。但其实在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的休眠状态。也就是说只要 Java 线程处于这三种状态之一,那么这个线程就永远没有 CPU 的使用权。所以 Java 线程的生命周期可以简化为下图:Java 中的线程状态转换图

什么是阻塞队列?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,让生产者和消费者能够高效率的进行通讯呢?JDK是使用通知模式实现的。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。

英文单词 synchronized

synchronized
英 ['sɪŋkrənaɪzd] 美 ['sɪŋkrənaɪzd]
adj. 同步的;同步化的
v. 使协调(synchronize的过去分词);同时发生;校准

概念

维基百科)
管程,对应的英文是 Monitor,很多 Java 领域的同学都喜欢将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”,这个是意译,而我自己也更倾向于使用“管程”。所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。那管程是怎么管的呢?
在并发编程领域,有两大核心问题:一个是互斥,即同一时刻只允许一个线程访问共享资源;另一个是同步,即线程之间如何通信、协作。这两大问题,管程都是能够解决的。

互斥的解决

管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列(阻塞队列)和相关的操作入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。不知你有没有发现,管程模型和面向对象高度契合的。估计这也是 Java 选择管程的原因吧。而我在前面章节介绍的互斥锁用法,其背后的模型其实就是它。

同步的解决

这个就比较复杂了,不过你可以借鉴一下我们曾经提到过的就医流程,它可以帮助你快速地理解这个问题。为进一步便于你理解,在下面,我展示了一幅 MESA 管程模型示意图,它详细描述了 MESA 模型的主要组成部分。在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。这个过程类似就医流程的分诊,只允许一个患者就诊,其他患者都在门口等待。管程里还引入了条件变量的概念,而且每个条件变量都对应有一个等待队列,如下图,条件变量 A 和条件变量 B 分别都有自己的等待队列。
管程模型
那条件变量和等待队列的作用是什么呢?其实就是解决线程同步问题。

图中有个队列没画出来,就是入队出队的阻塞队列

你也可以结合上面提到的入队出队例子加深一下理解。假设有个线程 T1 执行出队(阻塞队列)操作,不过需要注意的是执行出队操作,有个前提条件,就是队列不能是空的,而队列不空这个前提条件就是管程里的条件变量。 如果线程 T1 进入管程后恰好发现队列(阻塞队列)是空的,那怎么办呢?等待啊,去哪里等呢?就去条件变量对应的等待队列里面等。此时线程 T1 就去“队列不空”这个条件变量的等待队列中等待。这个过程类似于大夫发现你要去验个血,于是给你开了个验血的单子,你呢就去验血的队伍里排队。线程 T1 进入条件变量的等待队列后,是允许其他线程进入管程的。这和你去验血的时候,医生可以给其他患者诊治,道理都是一样的。再假设之后另外一个线程 T2 执行入队操作,入队操作执行成功之后,“队列不空”这个条件对于线程 T1 来说已经满足了,此时线程 T2 要通知 T1,告诉它需要的条件已经满足了。当线程 T1 得到通知后,会从等待队列里面出来,但是出来之后不是马上执行,而是重新进入到入口等待队列里面。这个过程类似你验血完,回来找大夫,需要重新分诊。条件变量及其等待队列我们讲清楚了。

其实就是阻塞队列是否可以入队,出队的条件

下面再说说 wait()、notify()、notifyAll() 这三个操作。前面提到线程 T1 发现“队列不空”这个条件不满足,需要进到对应的等待队列里等待。这个过程就是通过调用 wait() 来实现的。如果我们用对象 A 代表“队列不空”这个条件,那么线程 T1 需要调用 A.wait()。同理当“队列不空”这个条件满足时,线程 T2 需要调用 A.notify() 来通知 A 等待队列中的一个线程,此时这个队列里面只有线程 T1。至于 notifyAll() 这个方法,它可以通知等待队列中的所有线程。
这里我还是来一段代码再次说明一下吧。下面的代码实现的是一个阻塞队列,阻塞队列有两个操作分别是入队和出队,这两个方法都是先获取互斥锁,类比管程模型中的入口。对于入队操作,如果队列已满,就需要等待直到队列不满,所以这里用了notFull.await()。对于出队操作,如果队列为空,就需要等待直到队列不空,所以就用了notEmpty.await()。如果入队成功,那么队列就不空了,就需要通知条件变量:队列不空notEmpty对应的等待队列。如果出队成功,那就队列就不满了,就需要通知条件变量:队列不满notFull对应的等待队列。


public class BlockedQueue<T>{
  final Lock lock =  new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull = lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty =  lock.newCondition();

  // 入队
  void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满 
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}

在这段示例代码中,我们用了 Java 并发包里面的 Lock 和 Condition,如果你看着吃力,也没关系,后面我们还会详细介绍,这个例子只是先让你明白条件变量及其等待队列是怎么回事。需要注意的是:await() 和前面我们提到的 wait() 语义是一样的;signal() 和前面我们提到的 notify() 语义是一样的。

  • 管程是一模型啊,或者可以理解为synchronizd关键字的内部实现
  • 管理共享内存和操作共享内存的过程,令其支持并发。并发编程领域两大难题,互斥和同步。互斥如何保证:封装共享数据及其对应操作,每个操作任意时刻只允许一个线程执行。
  1. 管程是一种概念,任何语言都可以通用。
  2. 在java中,每个加锁的对象都绑定着一个管程(监视器)
  3. 线程访问加锁对象,就是去拥有一个监视器的过程。如一个病人去门诊室看医生,医生是共享资源,门锁锁定医生,病人去看医生,就是访问医生这个共享资源,门诊室其实是监视器(管程)。
  4. 所有线程访问共享资源,都需要先拥有监视器。就像所有病人看病都需要先拥有进入门诊室的资格。
  5. 监视器至少有两个等待队列。一个是进入监视器的等待队列一个是条件变量对应的等待队列。后者可以有多个。就像一个病人进入门诊室诊断后,需要去验血,那么它需要去抽血室排队等待。另外一个病人心脏不舒服,需要去拍胸片,去拍摄室等待。
  6. 监视器要求的条件满足后,位于条件变量下等待的线程需要重新在门诊室门外排队,等待进入监视器。就像抽血的那位,抽完后,拿到了化验单,然后,重新回到门诊室等待,然后进入看病,然后退出,医生通知下一位进入。
  7. 总结起来就是,管程就是一个对象监视器。任何线程想要访问该资源,就要排队进入监控范围。进入之后,接受检查,不符合条件,则要继续等待,直到被通知,然后继续进入监视器。

    代码实现

  8. 代码演示的阻塞队列是和生产者-消费者模型所用的队列一个道理
  9. 就是线程wait后,下次再到runnable状态的时候,是接着上次运行的那行继续执行的对吧 不是重头开始运行,接着上次运行的那一行继续执行
  • 这里的队列是阻塞队列的意思,顺便简单温习下阻塞队列(拿经典的例子生产者和消费来说): 比如生产者每生产一个产品就放到一个仓库中,消费者去这个仓库消费这个产品,对应到java中就是,产品属于共享数据,仓库属于队列。当仓库(队列)满了,就会让生产者先停止生产,就是让线程先阻塞,当消费者消费一个产品后,仓库(队列)不满了,就会唤醒一个线程(生产者)去生产产品;同理,当消费者太多 出现供不应求,就是仓库的产品被消费完了,就会让线程(消费者)阻塞,直到仓库(队列)中有产品为止,再重新唤醒消费者线程去消费。 对文中管程例子的理解:条件变量a,条件变量b是不是就可以理解为仓库没有产品,不能消费,仓库满了不能生产的情况条件变量是一个实实在在的变量
  • hasen 的策略是优先执行持有资源的操作,hoare优先执行被阻塞的操作,mesa有机会两个都同时执行,但也有可能被阻塞的不被执行
  1. 阻塞队列出队和入队操作的元素是共享变量?即队列里是什么,JVM共享一个阻塞队列吗?
  2. 管程中的阻塞队列的出队和入队,与线程给锁对象加锁和释放锁的关系

作者回复:

  1. 阻塞队列内部的数组或者链表是共享变量。阻塞队列就是个普通对象,作用域是你自己控制的。
  2. 管程内部的阻塞队列,不同的管程是不同的。
  3. 加锁释放锁,可能会操作入口等待队列。
  4. 讲到管程MESA模型,假设有个线程 T1 执行出队操作,T2执行入队操作,这个队列具体指的是哪个队列,是入口等待队列,还是条件变量对应的队列啊?
    作者回复: 和这俩队列没关系,只是个应用管程的例子而已
  5. 出队入队,具体是那个队列,没看明白,文中说有两个队列 作者回复: 指的是BlockedQueue,不是管程里的等待队列

精选留言

  • MESA模型和其他两种模型相比可以实现更好的公平性,因为唤醒只是把你放到队列里而不保证你一定可以执行,最后能不能执行还是要看你自己可不可以抢得到执行权也就是入口,其他两种模型是显式地唤醒,有点内定的意思了。
    作者回复: 内定都出来了,真是理论联系生活
  • 简单来说,一个锁实际上对应两个队列,一个是就绪队列,对应本节的入口等待队列,一个是阻塞队列,实际对应本节的条件变量等待队列,wait操作是把当前线程放入条件变量的等待队列中,而notifyall是将条件变量等待队列中的所有线程唤醒到就绪队列(入口等待队列)中,实际上哪个线程执行由jvm操作
  • 在MESA模型中,线程T1被唤醒,从条件A的等待队列中(其实是一个set,list的话可能会重复)移除,并加入入口等待队列,重新与其他的线程竞争锁的控制权。那么有这样一种可能,线程T1的优先级比较低,并且经常地有高优先级的线程加入入口等待队列。每次当它获得锁的时候,条件已经不满足了(被高优先级的线程抢先破坏了条件)。即使T1可以得到调度,但是也没办法继续执行下去。最后T1被饿死了(有点冷。。。)另外我刚才的问题想通了。不需要实现像lock一样的条件对象,并调用condition.await(). Synchronized用判断条件+wait()就可以了。
  • wait() 不加超时参数,相当于得一直等着别人叫你去门口排队,加了超时参数,相当于等一段时间,再没人叫的话,我就受不了自己去门口排队了,这样就诊的机会会大一点,是这样理解吧? 挺形象,就诊机会不一定大,但是能避免没人叫的时候傻等
  • 信号量机制是可以解决同步/互斥的问题的,但是信号量的操作分散在各个进程或线程中,不方便进行管理,因每次需调用PV操作,还可能导致死锁或破坏互斥请求的问题。管程是定义了一个数据结构和能为并发所执行的一组操作,这组操作能够进行同步和改变管程中的数据。这相当于对临界资源的同步操作都集中进行管理,凡是要访问临界资源的进程或线程,都必须先通过管程,由管程的这套机制来实现多进程或线程对同一个临界资源的互斥访问和使用。管程的同步主要通过condition类型的变量(条件变量),条件变量可执行操作wait()和signal()。管程一般是由语言编译器进行封装,体现出OOP中的封装思想,也如老师所讲的,管程模型和面向对象高度契合的。作者回复: 是的,管程只是一种解决并发问题的模型而已。
  • 管程的英文就是monitor,至于synchronized底层的实现还挺复杂的,尤其是1.6优化之后。你可以参考《java并发编程的艺术》的第二章,详细描述了具体实现。
  • 想起来前段时间看的AQS,很像是MESA的具体实现哈,不知道是否理解正确,以前看到监视器模型就没有深究它的理论模型,看来还是要追根溯源,这样才能真正理解,比如看了很多并发包的源码,但是现在加上老师的理论,理解起来会豁然开朗
  • 我觉得超时参数很有必要,老师说wait()的正确姿势是:

    while(条件不满足) {
    wait();
    }
    class Minitor{
    同步队列(一个)
    条件队列(多个)
    入队
    出队
    }

    有可能条件永远不满足,一直等下去

  • 关于思考题超时,这三个模型都是判断条件然后再执行,不同的是前两个都是马上执行,而MESA模型唤醒后是进入等待队列,如果队列为空,那跟前两个一样,如果不为空,则要考虑是否会等待过长时间,这样看更多的是考虑是给业务代码多一种选择吧

本节课总结:

安全性:

编写并发程序的初衷是为了提升性能,但在追求性能的同时由于多线程操作共享资源而出现了安全性问题,所以才用到了锁技术,一旦用到了锁技术就会出现了死锁,活锁等活跃性问题,而且不恰当的使用锁,导致了串行百分比的增加,由此又产生了性能问题,所以这就是并发程序与锁的因果关系。
  • 数据竞争: 多个线程同时访问一个数据,并且至少有一个线程会写这个数据。
  • 竞态条件: 程序的执行结果依赖程序执行的顺序。
    也可以按照以下的方式理解竞态条件: 程序的执行依赖于某个状态变量,在判断满足条件的时候执行,但是在执行时其他变量同时修改了状态变量。也就是不能保证原子性

    if (状态变量 满足 执行条件) {
    执行操作
    }

    问题: 数据竞争一定会导致程序存在竞态条件吗?有没有什么相关性?


public class Test {
  private long count = 0;
  synchronized long get(){
    return count;
  }
  synchronized void set(long v){
    count = v;
  } 
  //这里是不同步的,虽然add和set是同步的,但是放在一起不能保证原子性
  void add10K() {
    int idx = 0;
    while(idx++ < 10000) {
      set(get()+1);      
    }
  }
}

get虽然有锁,只能保证多个线程不能同一时刻执行。但是出现不安全的可能是线程a调用get后线程b调用get,这时两个get返回的值是一样的。然后都加一后再分别set.这样两个线程就出现并发问题了。问题在于同时执行get,而在于get和set是两个方法,这两个方法组合不是原子的,就可能两个方法中间的时间也有其它线程分别调用,出现并发问题

关于add10()的理解是这样的,第一个线程拿到了count等于1,还没执行set操作的时候,第二个线程拿到了count等于1,这两个线程都将count等于写入到各自的寄存器里面,这时候发生了任务切换,因为不是原子的,所以会导致最后写到内存的count等于1而不是想要的2


class Account {
  private int balance;
  // 转账
  void transfer(
      Account target, int amt){
    if (this.balance > amt) {
      this.balance -= amt;
      target.balance += amt;
    }
  } 
}
转账这个例子如果不加措施,因为可见性,两个线程彼此看不见对方,初始值都>150 然后都减150变成50,或者因为任务切换 切换到另一个线程减了150也是50
会存在一个线程减到50,另一个线程从50开始减嘛?也就是一个减操作结束后刷到内存,另一个做减法的时候又从内存读值
购买书籍 《Java并发编程的艺术》

另外想到SpringFrameWork中各种模板类都是线程安全的,模板类访问数据不同的持久化技术要绑定不同的会话资源,这些资源本身不是线程安全的。多线程环境下使用synchronized会降低并发,正是使用了ThreadLocal杜绝了不用线程同步情况下解决了线程安全的问题。

活跃性:

  • 死锁:破坏造成死锁的条件,

    • 1,使用等待-通知机制的Allocator;
    • 2主动释放占有的资源;
    • 3,按顺序获取资源。
  • 活锁:虽然没有发生阻塞,但仍会存在执行不下去的情况。我感觉像进入了某种怪圈。解决办法,等待随机的时间,例如Raft算法中重新选举leader。
  • 饥饿:我想到了没有引入时间片概念时,cpu处理作业。如果遇到长作业,会导致短作业饥饿。如果优先处理短作业,则会饿死长作业。长作业就可以类比持有锁的时间过长,而时间片可以让cpu资源公平地分配给各个作业。当然,如果有无穷多的cpu,就可以让每个作业得以执行,就不存在饥饿了。
互斥锁本质上是将并行的程序串行化

性能:

核心就是在保证安全性和活跃性的前提下,根据实际情况,尽量降低锁的粒度。即尽量减少持有锁的时间。JDK的并发包里,有很多特定场景针对并发性能的设计。还有很多无锁化的设计,例如MVCC,TLS,COW等,可以根据不同的场景选用不同的数据结构或设计。

最后,在程序设计时,要从宏观出发,也就是关注安全性,活跃性和性能。遇到问题的时候,可以从微观去分析,让看似诡异的bug无所遁形。

public class Allocator {
    private final List<Account> als = new LinkedList<Account>();
    // 一次性申请所有资源
    public synchronized void apply(Account from, Account to) {
        //一开始也是觉得while条件写反了,后来理解了。Allocator 是管理员分配者,而不是申请者
        // 经典写法
        while (als.contains(from) || als.contains(to)) {
            try {
                System.out.println("等待用户 -> " + from.getId() + "_" + to.getId());
                wait();
            } catch (Exception e) {
                //notify + notifyAll 不会来这里
                System.out.println("异常用户 -> " + from.getId() + "_" + to.getId());
                e.printStackTrace();
            }
        }
        als.add(from);
        als.add(to);
    }
    // 归还资源
    public synchronized void free(Account from, Account to) {
        System.out.println("唤醒用户 -> " + from.getId() + "_" + to.getId());
        als.remove(from);
        als.remove(to);
        notifyAll();
    }
}


public class Account {
    // actr 应该为单例
    private final Allocator actr;
    //唯一账号
    private final long id;
    //余额
    private int balance;

    public Account(Allocator actr, long id, int balance) {
        this.actr = actr;
        this.id = id;
        this.balance = balance;
    }

    // 转账
    public void transfer(Account target, int amt) {
        // 一次性申请转出账户和转入账户,直到成功
        actr.apply(this, target);
        try {
            //TODO 有了资源管理器,这里的synchronized锁就不需要了吧?!
            if (this.balance > amt) {
                this.balance -= amt;
                target.balance += amt;
            }
            //模拟数据库操作时间
            try {
                Thread.sleep(new Random().nextInt(2000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            actr.free(this, target);
        }
    }

    public long getId() {
        return id;
    }
}

等待通知示意图

文章中的左边和右边的两个队列应该改一改名字,不应该都叫等待队列,这样对新手很容易产生误解。如果左边的叫做同步队列右边的叫做等待队列可能更好。左边的队列是用来争夺锁的,右边的队列是等待队列,是必须被notify的,当被notify之后,就会被放入左边的队列去争夺锁

尽量使用 notifyAll()

在上面的代码中,我用的是 notifyAll() 来实现通知机制,为什么不使用 notify() 呢?这二者是有区别的,

  • notify() 是会随机地通知等待队列中的一个线程
  • notifyAll() 会通知等待队列中的所有线程。
从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到。假设我们有资源 A、B、C、D,线程 1 申请到了 AB,线程 2 申请到了 CD,此时线程 3 申请 AB,会进入等待队列(AB 分配给线程 1,线程 3 要求的条件不满足),线程 4 申请 CD 也会进入等待队列。我们再假设之后线程 1 归还了资源 AB,如果使用 notify() 来通知等待队列中的线程,有可能被通知的是线程 4,但线程 4 申请的是 CD,所以此时线程 4 还是会继续等待,而真正该唤醒的线程 3 就再也没有机会被唤醒了。所以除非经过深思熟虑,否则尽量使用 notifyAll()。

wait()和sleep区别

  • wait()方法与sleep()方法的不同之处在于,wait()方法会释放对象的“锁标志”。当调用某一对象的wait()方法后,会使当前线程暂停执行,并将当前线程放入对象等待池中,直到调用了notify()方法后,将从对象等待池中移出任意一个线程并放入锁标志等待池中,只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。当调用了某个对象的notifyAll()方法,会将对象等待池中的所有线程都移动到该对象的锁标志等待池。
  • sleep()方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是sleep()方法不会释放“锁标志”,也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。
  1. wait会释放所有锁而sleep不会释放锁资源.
  2. wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
  3. wait无需捕捉异常,而sleep需要.(都抛出InterruptedException ,wait也需要捕获异常)
  4. wait()无参数需要唤醒,线程状态WAITING;wait(1000L);到时间自己醒过来或者到时间之前被其他线程唤醒,状态和sleep都是TIME_WAITING
  5. 两者相同点:都会让渡CPU执行时间,等待再次调度

新毛笔的开笔

零基础的同学,拿到毛笔后,不要用热水泡笔。热水可能使笔根脱胶,导致笔头松动掉落。直接用冷水蘸湿,用手指捻开笔毫就行。整个过程一分钟搞定。笔开不好,一开始就废了。为保险起见,先不要自己写字哈。