常见多线程问题

  • 为什么 wait 方法必须在 synchronized 保护的同步代码中使用?

    从api文档入手,文档中明确告知使用wait方法必须写在synchronized保护的while循环代码块中。正面回答不好解释,可以从反面解释这个问题。举一个生产者和消费者的例子,一个类中give方法负责生产数据,生产完成一个数据后通知消费者消费数据;类中的take方法负责消费数据,在消费数据前先判断队列是否有数据,如果有数据则消费数据,如果没有数据则进入等待生产者生车数据。 a、如果没有synchronized代码块,在多线程协调处理数据的情况下,很容易出现下列情况,消费者在执行判断数据是否为空的条件后,线程执行的资源被操作系统调度去执行其他任务,还未执行wait方法; b、此时生产者在生产数据,生产结束生产者线程进入阻塞,后通知消费者线程消费数据,但是由于消费者线程未执行wait方法,通知并不会产生任何结果。 c、操作系统调度资源继承执行生产者线程的wait方法,生产者进入等待。此时如果没有其他生产者生产数据的情况下,消费者线程就会进入无尽等待。 1、造成以上问题的原因是“判断-执行 ”不是原子操作,中间被打断了,是线程不安全的。

2、在线程操作中,还存在一种“虚假唤醒”,线程可能在既没有被notify/notifyAll,也没有被终端或超时唤醒的情况。这种唤醒是不希望发生的,所以为了避免这种情况,需要采用while循环接口每次都检查条件是否满足。

  • 为什么 wait/notify/notifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?

    1、因为 Java 中每个对象都有一把称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。

2、因为如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。

  • wait/notify 和 sleep 方法的异同?

    相同点: 1、它们都可以让线程阻塞; 2、它们都可以响应interrupt中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出interruptedException异常; 异同点: 1、wait方法必须在synchronized保护的代码块中使用,而sleep方法则没有这个要求; 2、在同步代码块中执行sleep方法,并不会释放monitor锁对象,但执行wait方法时会主动释放monitor锁对象; 3、sleep方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的wait方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复; 4、wait/notify方是Object类的方法,而sleep方法是Thread类的方法;

知识点速记

  • 线程安全问题产生的简单理解
    • 在多线程环境下,cpu的调度是以时间片为单位进行分配的,每个线程都可以分配得到一定量的时间片,但如果线程拥有的时间片耗尽,它将会被暂停执行并让出cpu资源给其他线程,这样就有可能发生线程安全问题。
  • 线程池
    • 线程池类:ThreadPoolExecutor
    • 线程拒绝线程任务接口:RejectedExecutionHandler
    • 拒绝策略
      • 第一种拒绝策略是 AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出一个类型为 RejectedExecutionException 的 RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略。
      • 第二种拒绝策略是 DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失。
      • 第三种拒绝策略是 DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
      • 第四种拒绝策略是 CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这样做主要有两点好处。
        • 第一点新提交的任务不会被丢弃,这样也就不会造成业务损失。
        • 第二点好处是,由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。