线程基本知识

  • 多线程的必要性
    • 利用多核CPU
    • 利用阻塞时的空闲CPU资源
    • 均分计算资源,让多个任务能同时推进,而不是只服务一个客户。

基本操作

  • 线程的创建:
    • Thread
    • Runnable
  • Thread.yield() --> yield means放手,放弃。一个调用yield方法的线程虚拟机它愿意让出processor。Scheduler可以忽视这个hint。
    • 当前线程从 running 到runnable 状态。(也可能是 running 到 running)
    • Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。(exactly equal)
    • Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态。因为让步的线程还有可能被线程调度程序再次选中。
  • Thread#setDaemon(true)  这是一个实例方法
    • 所有的非守护线程退出时程序结束,即使还有守护线程
  • Thread#join()  

线程状态

  • new
  • runnable: 线程被创建后,其他线程调用了该对象的 start() 方法后,该线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。
  • running: runnable的线程获得了cpu时间片(timeslice),执行程序代码。
  • waiting(无限期等待): 处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显示地唤醒。以下方法会让线程陷入无限期等待状态:
    • 未设置Timeout参数的 Object.wait() 方法
    • 未设置Timeout参数的 Thread.join() 方法
    • LockSupport.park() 方法
  • Timed Wainging(限期等待): 处于该状态的线程也不会被分配CPU执行时间,不过无须等待被其他线程显式地唤醒,在一定时间后它们会由系统自动唤醒。
    • Thread.sleep()
    • 设置了Timeout参数的 Object.wait() 方法
    • 设置了Timeout参数的 Thread.join() 方法
    • LockSupport.parkNanos()
    • LockSupport.parkUntil()
  • Blocked: 阻塞与等待的区别在于,阻塞状态在等待一个排他锁,这个事件将在另一个线程放弃这个锁的时候发生,而等待状态是在等待一段时间or唤醒动作的发生。
  • dead: 线程run()方法执行结束 or 因异常退出,则该线程结束生命周期。
  • 对线程状态还有一种分法如下:
  • block: 阻塞状态是指线程因为某种原因放弃了cpu使用权,也即出让了cpu timeslice,暂时停止运行。进入block状态后,只有线程进入runnable才有机会再次获得timeslice转到running。阻塞的情况分三种
    • 等待阻塞:running的线程执行 o.wait() 方法,JVM会把该线程放入等待队列(waiting queue)中。
    • 同步阻塞:running的线程在获取对象的同步锁时,若该同步锁被其他线程占用,则JVM会把该线程放入锁池(block pool)中。
    • 其它阻塞:running的线程执行Thread.sleep() or t.join() or 发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep超时 or join等待线程终止或超时 or I/O处理完毕时,线程重新转入runnable状态。

线程中断

  • 线程中断
    • 相关的几个方法
      • Thread#interrput() 可用来中断某个线程
      • Thread#isInterrputed() 返回线程的中断标志位
      • Thread.interrputed() 返回当前线程的中断标志位,并重置。
    • 每个线程都有一个interrupt status标志位,用于表明当前线程是否处于中断状态。
    • 对中断的响应
      • 若线程处于 可中断的阻塞状态(即 WAITING / TIMED_WAITING状态),则复位中断标志位,立即取消阻塞状态,并抛出 InterruptedException(这也是这些方法签名会抛出InterruptedException的原因)。
      • 其他情况下,仅设置其中断标志位,需要线程先通过Thread#isInterrrupted()Thread.interrupted()查询再处理。
    • 因而,中断是一种协作机制,interrupt一个线程不是粗鲁地立即停止其当前正在进行的事情,而是请求该线程在它愿意并方便的时候停止它的执行,这种请求可能是粗暴的(抛出InterruptedException),也可能是温和的(仅设置中断标志位)。
      • 为什么这么设计:中断使得我们可以更安全地取消任务:不负责任地立即杀死一个线程可能导致资源的泄露、事务的不完整或业务的缺失等等,需要给被中断线程一个机会在退出之前进行必要的清理工作。
    • 处理中断:被中断线程可以用任意方式处理中断信号,对于非阻塞但耗时较长的操作,可以轮询中断状态位,在被中断的时候执行必要的逻辑并退出。

互斥和协作

  • 互斥:二元lock保证线程之间的互斥,让线程顺序地进入临界区,保证线程不会观察到其他线程操作的中间状态
  • 协作:condition则用于线程间的协作,当某个线程发现不满足时主动进入阻塞,直到其他线程修改了条件并将其唤醒。条件的测试和修改都要锁保证互斥,因此几乎所有的实现中condition都是和一个锁绑定在一起,工作在一个锁的上下文中的。
  • java语言层面提供了内置的 lock(monitor) + condition 组合, 如下:
    Object lock = new Object();
            synchronized (lock) {
                while (true) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
    

    也就是 Object#wait() + notify() / notifyAll() 机制。每个Object都内置一把锁,该锁内部有且只有一个隐含的condition。

监视器

  • 监视器好比一座建筑,它有一个特别的房间,房间内有一些数据,在同一时间只能被一个线程占据。(是不是像临界区的定义=。=)
  • 监视器monitor保证同一时间只能有一个线程可以访问特殊的数据和代码。
  • monitor是java提供的一种对锁的封装;monitor可以保证所有线程互斥的获得一个锁;monitor可以让出对锁的权限并且等待一个条件去重新获得这个锁;monitor可以发出信号通知别的线程它们等待的条件被满足了。
  • 实现:可以通过os中的mutex 和 semaphore 来实现,前者保证同步,后者保证互斥。(这两者都需要硬件来支持原子性)
    • mutex如何实现monitor的condition功能?
      • 上锁,使用mutex.acqurie()
      • wait,维护一个waitingQueue,
        • 把当前线程加入这个队列
        • mutex.release()交出锁的控制权
        • sleep进入休眠状态,被唤醒时重新mutex.acquire()去获得锁
      • signal,维护一个readyQueue,将waitingQueue里面的队头取出来加入readyQueue,由os去唤醒readyQueue里面的线程
  • 注意上面的图:
    1. 一个线程通过1号门进入entry set(入口区),如果在入口区没有线程等待,那么这个线程就会获取监视器,称为监视器的owner,然后执行监视区域的代码。
      • 锁是访问对象时,通过对对象加锁,防止并行访问的控制手段。对象加锁成功,才能拿到 object monitor,否则会进入对象监视器的 entry 队列(monitor entry list),表现为 waiting for monitor entry, java.lang.Thread.State: BLOCKED(on object monitor).
    2. 线程在持有监视器的过程中有两种选择
        • 正常执行直到释放监视器,通过5号门退出监视器
      等待某个条件的出现
          ,于是它会通过3号门到
      wait set(等待区)
        休息,直到满足相应的条件后在通过4号门进入重新获取监视器再执行。
        • 对对象加锁成功,调用wait方法,也会wait在object monitor上,但会释放锁,并进入 monitor wait list,当前线程也就 WAITING (on object monitor), 表现为 java.lang.Thread.State: WAITING(on object monitor) 一旦对象被调用notify,会重新尝试加锁,成功则可以执行,否则进入 monitor entry list。
  • 关于线程使用对象的方式:
    • 直接(独占)使用,为避免多个线程同时使用,需要加锁,加锁成功的可以使用对象。[obj as lock]
    • 需要对象达到某种状态才能用,这时候需要调用对象的wait方法,线程挂起(wait)在对象的object monitor监视器上,等待其他线程notify,可能多个线程都在wait,所以notify的时候多个等待的线程需要再次获取锁。 [obj as condition queue]
  • 关于为什么要先获取对象的锁,才能wait / notify: 
    • 一个对象的固有锁和它的固有条件队列是相关的,为了调用对X内条件队列的方法,你必须获得对象X的锁。这是因为等待状态条件的机制和保证状态连续性的机制是紧密结合在一起的。
    • 在设计原理上来说,是因为如果没有锁,wait和notify有可能会产生竞态条件(Race Condition)
      • 考虑一个生产者消费者问题
  • 那不事先获取锁会怎样?
    • public static void main(String[] args) throws InterruptedException{
              Object obj = new Object();
              obj.wait();
          }
      
      
      // will throw java.lang.IllegalMonitorStateException
    • 以及一定是要获取相对应的锁:
      public static void main(String[] args) throws InterruptedException{
              Object obj = new Object();
              Object lock = new Object();
              synchronized (lock) {
                  obj.wait();
              }
          }
      
      
      // also throws java.lang.IllegalMonitorStateException
      
  •  object's monitor 的缺点:
    • 没办法实现公平锁
    • 一个锁没办法使用多个condition
    • 没办法中断wait操作

    那基于上述缺点,我们可以通过juc的locks.Lock来避免,具体就看下面相关的部分啦。

Monitor VS Lock

  • 他俩本质上就不是同一个词
  • A "lock" is something with acquire and release primitives that maintain certain lock properties; e.g. exclusive use or single writer / multiple reader.
  • A "monitor" is a mechanism that ensures that only one thread can be executing a given section (or sections) of code at any given time. This can be implemented using a lock (and "condition variables" that allow threads to wait for or send notifications to other threads that the condition is fulfilled), but it is more than just a lock. Indeed, in the Java case, the actual lock used by a monitor is not directly accessible. (You just can't say "Object.lock()" to prevent other threads from acquiring it ... like you can with a Java Lock instance.)
    简而言之,monitor是一种保证在任意时刻只有一个thread可以访问某一块区域的,机制。因而,它可以用lock来实现,当然也可以用条件变量实现。在Java编程层面,你不能直接访问monitor所使用的lock。(那这么说的话,感觉monitor感觉是java语言上层的抽象,底层还是通过lock来实现。当然lock不是唯一实现的方式。)
  • 看下wiki中关于monitor的def:In concurrent programming, a monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait (block) for a certain condition to become true. A monitor consists of a mutex (lock) object and condition variables.

Summary

  • 如下是个人对于java monitor的总结
  1. java monitor 上java层面的一种封装
  2. 可以实现 互斥 or 同步 --> 本质是通过 lock 以及 condition
  3. 可以理解为每个object上附带有一个 lock 和一个 condition queue --> 所以想象一个要更新这个 condition queue,还是要先获取lock吧,否则会出现竞态条件 > <

 

JUC.locks.Lock

  • 底层是j.u.c的几大子模块
    • Lock: 锁,实现和synchronized关键字相同的功能和语义
    • Synchronizer: 同步器,保证线程同步,就是按照预定的先后顺序进行,线程同步的机制主要有:临界区、互斥量、事件、信号量四种方式。
    • BlockingQueue: 阻塞队列。常用于生产者消费者场景。
    • Executor: 执行器,与线程池相关
    • 并发容器: 线程安全的并发访问容器。
  • 中间层
    • AQS: AbstractQueueSynchronizer抽象队列同步器。AQS是JUC同步器的基石。AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock、Condition 、Semaphore 、ReentrantReadWriteLock 、CyclicBarrier 、CountDownlatch
    • 非阻塞数据结构:基础数据结构
    • 原子变量类:java.util.concurrent.atomic包下的原子变量类
  • 最底层:
    • volatile: 保证共享变量的可见性 + 有序性(禁止指令重排序)
    • CAS: CompareAndSwap,一种基于硬件的乐观锁实现。

Lock

ReentrantLock

  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义。
  • 但在synchronized的基础上,添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。
  • 注意,lock必须在finally块中释放。

Condition

  • condition:线程通信更高效的方式
  • condition可以替代Object监视器(wait & notify)使用。
  • 使用上:condition必须与lock绑定。
  • await() 对应 obj.wait(),实际上会先释放锁,然后挂起线程(这里貌似是自旋等待),一旦条件满足就被唤醒,再次去获取锁。
  • condition最大的好处:可以为每个对象提供多个等待set(wait-set)。其中,lock替代了synchronized方法和语句的使用,condition替代了object监视器方法的使用。
  • 看下面的例子:对读写线程,如果我们只有一个condition,那么每次唤醒的时候,lock不知道唤醒的是读or写线程。比如说满的时候,唤醒了写线程,那么写线程被唤醒后立马被阻塞,会浪费很多时间。
    import java.util.concurrent.locks.Condition;
    import java.util.concurrent.locks.Lock;
    import java.util.concurrent.locks.ReentrantLock;
    
    
    public class ConditionTest {
        final Lock lock = new ReentrantLock();
        final Condition notFull = lock.newCondition(); // condition should binding to lock.
        final Condition notEmpty = lock.newCondition();
    
        final Object[] items = new Object[100];
        int putptr, takeptr, count;
    
        public void put(Object x) throws InterruptedException {
            lock.lock();
            try {
                while (count == items.length) notFull.await();  // blocking write process
                items[putptr] = x;
                if (++putptr == items.length) putptr = 0;
                ++count;
                notEmpty.signal();
            } finally {
                lock.unlock();
            }
        }
    
        public Object take() throws InterruptedException {
            lock.lock();
    
            try {
                while (count == 0) notEmpty.await();
                Object res = items[takeptr];
                if (++takeptr == items.length) takeptr = 0;
                --count;
                notFull.signal();
                return res;
            } finally {
                lock.unlock();
            }
        }
    }

锁优化

  • Lock-free算法,避免锁和阻塞
  • 尽可能减少临界区长度

Lock-Free算法

  • 核心是 自旋 + 观察旧值 & 计算新值 + CAS(旧值,新值)。假设在用CAS更新时,如果CAS失败了,则说明有其他线程并发地在修改,此时线程不阻塞,而是不停地重试直到成功。
  • 这也就是传说中的乐观锁啦。
  • 实例:JUC的 AtomicInteger 等原子类正式用CAS保证线程安全的。
  • 适用于竞争较少,且临界区短的场景。否则会造成大量的CPU空转,浪费CPU资源。

Lock-Free的并发数据结构

  • JUC中,基于非阻塞算法实现的并发容器包括:ConcurrentLinkedQueue,SynchronousQueue,Exchanger 和 ConcurrentSkipListMap

 

死锁

  • 死锁的四个必要条件
    • 资源独占:资源的使用是互斥的
    • 不可剥夺:不可强行从资源占用着中夺取
    • 请求与保持:申请资源的时候同时保持对资源的占有
    • 循环等待:若干线程同时持有的资源与请求的资源组成一个回路

Solution

  • 在内部锁中,死锁是致命的:
    • 唯一的恢复方法是重新启动程序
    • 唯一的预防方法是在构建程序的时候不要出错
  • 而可轮询的锁(tryLock())获取模式具有更完善的错误恢复机制,可以规避死锁的发生。
  • 其实restart?循环中的某一个thread即可。那我们可以设置timeout。 

FYI