1.1. Java内存模型

1.1.1. JMM 的作用是什么

Java 线程的通信由 JMM 控制,JMM 的主要目的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与方法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循一个基本原则:只要不改变程序执行结果,编译器和处理器怎么优化都行。例如编译器分析某个锁只会单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。

JMM 规定所有变量都存储在主内存,每条线程有自己的工作内存,工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。不同线程间无法直接访问对方工作内存中的变量,线程通信必须经过主内存。

1.1.2. happens-before 是什么?

Happens-Before 规则:前面一个操作的结果,对后面一个操作可见。。

  • 程序次序规则:一个线程内写在前面的操作先行发生于后面的。
  • 管程锁定规则: unlock 操作先行发生于后面对同一个锁的 lock 操作。
  • volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
  • 线程启动规则:线程的 start 方法先行发生于线程的每个动作。
  • 线程终止规则:线程中所有操作先行发生于对线程的终止检测。
  • 对象终结规则:对象的初始化先行发生于 finalize 方法。
  • 传递性:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C 。

1.1.3. 可见性、原子性和有序性

  • 可见性:一个线程对共享变量的修改,另一个线程可见,称为可见性。造成可见性的原因是CPU缓存。
  • 原子性:一个或多个操作在CPU执行过程中不被终端,称为原子性。造成原子性的原因是 高级语言一个语句可能是多个CPU指令。
  • 有序性:程序按照代码的先后顺序执行,称为有序性。造成有序性的原因是 指令优化。

1.2. 锁

1.2.1. 悲观锁与乐观锁

  • 悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。Java中,synchronized关键字和Lock的实现类都是悲观锁。
  • 乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作(例如报错或者自动重试)。乐观锁在Java中是通过使用无锁编程来实现,最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。

image-20200901155950266

使用范围:

  • 悲观锁适合写操作多的场景,先加锁可以保证写操作时数据正确。
  • 乐观锁适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。使用原子类Java并发_CAS原理详细分析

1.2.2. 自旋锁与自适应自旋锁

自旋锁产生原因:在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

image-20200901172738421

自旋锁在JDK1.4.2中引入,使用-XX:+UseSpinning来开启。JDK 6中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

自适应意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

1.2.3. synchronized

  • 无锁:无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。无锁的特点就是修改操作在循环内进行,线程会不断的尝试修改共享资源。如果没有冲突就修改成功并退出,否则就会继续循环尝试。如果有多个线程修改同一个值,必定会有一个线程能修改成功,而其他修改失败的线程会不断重试直到修改成功。上面我们介绍的CAS原理及应用即是无锁的实现。无锁无法全面代替有锁,但无锁在某些场合下的性能是非常高的。
  • 偏向锁:偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。在大多数情况下,锁总是由同一线程多次获得,不存在多线程竞争,所以出现了偏向锁。其目标就是在只有一个线程执行同步代码块时能够提高性能。当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。在线程进入和退出同步块时不再通过CAS操作来加锁和解锁,而是检测Mark Word里是否存储着指向当前线程的偏向锁。引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令即可。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态。偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
  • 轻量级锁:是指当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,然后拷贝对象头中的Mark Word复制到锁记录中。拷贝成功后,虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象的Mark Word。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。若当前只有一个等待线程,则该线程通过自旋进行等待。但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。
  • 重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
  • 全流程总结
    • 成为偏向锁 一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的ThreadID改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。
    • 成为轻量级锁 一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象是偏向状态,这时表明在这个对象上已经存在竞争了,检查原来持有该对象锁的线程是否依然存活,如果挂了,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁,(偏向锁就是这个时候升级为轻量级锁的)。假如一个对象先被线程1加锁解锁,再被线程2加锁解锁,这过程中没有锁冲突,也一样会发生偏向锁失效,不同的是这回要先退化为无锁的状态,再加轻量锁,如果是线程1持有锁,且2也要争夺偏向锁,则直接到轻量级锁状态
    • 成为重量级锁 轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

1.2.4. 公平锁与非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大。
  • 非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。

1.2.5. 可重入锁 VS 非可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。

1.2.6. Lock与synchronized区别

1.2.7. Lock、tryLock、lockInterruptibly

  • lock :调用后一直阻塞到获得锁
  • tryLock :尝试是否能获得锁 会立即返回获取结果
  • lockInterruptibly :调用后一直阻塞到获得锁 但是接受中断信号
  • https://www.zhihu.com/question/36771163

1.3. 线程与进程

1.3.1. 线程与进程的区别

  • 进程:进程就是正在执行的程序,是操作系统资源分配的基本单位。一般来说,进程包含指令、数据和PCB
  • 线程:线程是进程内部的不同的执行路径,是操作系统独立调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。比如说,微信和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
  • 区别:
    • 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属于进程的资源。
    • 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
    • 开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
    • 通信:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC
  • 补充:
    • 内核线程:对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
    • 用户线程:对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
    • 孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程ID1的进程)所收养,并由 init 进程对它们完成状态收集工作。因为孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
    • 僵尸进程:一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait()waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait()waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。僵尸进程通过 ps 命令显示出来的状态为 Z
    • 守护进程:运行在后台的一种特殊进程,它是独立于控制终端的,并周期性地执行某些任务。

1.3.2. 线程的生命周期有哪些状态?

  • NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
  • RUNNABLE:Java 将操作系统中的就绪和运行两种状态统称为 RUNNABLE,此时线程有可能在等待时间片,也有可能在执行。
  • BLOCKED:阻塞状态,可能由于锁被其他线程占用、调用了 sleepjoin 方法、执行了 wait方法等。
  • WAITING:等待状态,该状态线程不会被分配 CPU 时间片,需要其他线程通知或中断。可能由于调用了无参的 waitjoin 方法。
  • TIME_WAITING:限期等待状态,可以在指定时间内自行返回。导可能由于调用了带参的 waitjoin 方法。
  • TERMINATED:终止状态,表示当前线程已执行完毕或异常退出。

1.3.3. 线程状态切换

  • new >> runnable :调用start方法
  • runnable >> TERMINATED:抛出异常、stop方法(弃用,因为会立即结束)、interrupt()
  • RUNNABLE >> TIMED_WAITING:调用带超时参数的 Thread.sleep(long millis) 方法;获得 synchronized 隐式锁的线程,调用带超时参数的 Object.wait(long timeout) 方法;调用带超时参数的 Thread.join(long millis) 方法;调用带超时参数的 LockSupport.parkNanos(Object blocker, long deadline) 方法;调用带超时参数的 LockSupport.parkUntil(long deadline) 方法。
  • RUNNABLE >> WAITING:Object.wait()、Thread.join()、LockSupport.park()
  • RUNNABLE >> BLOCKED:synchronized

image-20200901231508796

1.3.4. 线程数量

  • I/O 密集型:最佳线程数 =1 +(I/O 耗时 / CPU 耗时)
    • 多核:最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]
  • CPU 密集型: 线程的数量 =CPU 核数

1.3.5. 线程的创建方式有哪些?

  1. 继承 Thread 类并重写 run 方法。实现简单,但不符合里氏替换原则,不可以继承其他类。
  2. 实现 Runnable 接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
  3. 实现 Callable 接口并重写 call 方法。可以获取线程执行结果的返回值,并且可以抛出异常。

1.3.6. 线程有哪些方法?

  1. sleep 导致当前线程进入休眠状态,与 wait 不同的是该方法不会释放锁资源,进入的是 TIMED-WAITING 状态。
  2. yiled 方法使当前线程让出 CPU 时间片给优先级相同或更高的线程,回到 RUNNABLE 状态,与其他线程一起重新竞争CPU时间片。
  3. join 方法用于等待其他线程运行终止,如果当前线程调用了另一个线程的 join 方法,则当前线程进入阻塞状态,当另一个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间片。底层使用的是wait,也会释放锁。

1.3.7. 线程通信的方式有哪些?

命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型里线程间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信。在消息传递的并发模型里线程间没有公共状态,必须通过发送消息来显式通信。Java 并发采用共享内存模型,线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

  • volatile 告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可见性。
  • synchronized 确保多个线程在同一时刻只能有一个处于方法或同步块中,保证线程对变量访问的原子性、可见性和有序性。
  • 等待通知机制指一个线程 A 调用了对象的 wait 方法进入等待状态,另一线程 B 调用了对象的 notify/notifyAll 方法,线程 A 收到通知后结束阻塞并执行后序操作。对象上的 waitnotify/notifyAll 如同开关信号,完成等待方和通知方的交互。如果一个线程执行了某个线程的 join 方法,这个线程就会阻塞等待执行了 join 方法的线程终止,这里涉及等待/通知机制。join 底层通过 wait 实现,线程终止时会调用自身的 notifyAll 方法,通知所有等待在该线程对象上的线程。
  • 管道 IO 流用于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于生产者,PipedInputStream 和 PipedReader 是输入流,相当于消费者。管道流使用一个默认大小为 1KB 的循环缓冲数组。输入流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组首次为空时,输入流所在线程阻塞。
  • ThreadLocal 是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

1.3.8. 线程池有什么好处?

  • 降低资源消耗,复用已创建的线程,降低开销、控制最大并发数。
  • 隔离线程环境,可以配置独立线程池,将较慢的线程与较快的隔离开,避免相互影响。
  • 实现任务线程队列缓冲策略和拒绝机制。
  • 实现某些与时间相关的功能,如定时执行、周期执行等。

1.3.9. 线程池处理任务的流程?

  1. 核心线程池未满,创建一个新的线程执行任务,此时 workCount < corePoolSize。
  2. 如果核心线程池已满,工作队列未满,将线程存储在工作队列,此时 workCount >= corePoolSize。
  3. 如果工作队列已满,线程数小于最大线程数就创建一个新线程处理任务,此时 workCount < maximumPoolSize,这一步也需要获取全局锁。
  4. 如果超过大小线程数,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize。

线程池创建线程时,会将线程封装成工作线程 Worker,Worker 在执行完任务后还会循环获取工作队列中的任务来执行。

1.3.10. 有哪些创建线程池的方法?

可以通过 Executors 的静态工厂方法创建线程池:

  • newFixedThreadPool,固定大小的线程池,核心线程数也是最大线程数,不存在空闲线程,keepAliveTime = 0。该线程池使用的工作队列是无界阻塞队列 LinkedBlockingQueue,适用于负载较重的服务器。
  • newSingleThreadExecutor,使用单线程,相当于单线程串行执行所有任务,适用于需要保证顺序执行任务的场景。
  • newCachedThreadPool,maximumPoolSize 设置为 Integer 最大值,是高度可伸缩的线程池。该线程池使用的工作队列是没有容量的 SynchronousQueue,如果主线程提交任务的速度高于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程而耗尽CPU 和内存资源。适用于执行很多短期异步任务的小程序或负载较轻的服务器。
  • newScheduledThreadPool:线程数最大为 Integer 最大值,存在 OOM 风险。支持定期及周期性任务执行,适用需要多个后台线程执行周期任务,同时需要限制线程数量的场景。相比 Timer 更安全,功能更强,与 newCachedThreadPool 的区别是不回收工作线程。
  • newWorkStealingPool:JDK8 引入,创建持有足够线程的线程池支持给定的并行度,通过多个队列减少竞争。

1.3.11. 创建线程池有哪些参数?

  1. corePoolSize:常驻核心线程数,如果为 0,当执行完任务没有任何请求时会消耗线程池;如果大于 0,即使本地任务执行完,核心线程也不会被销毁。该值设置过大会浪费资源,过小会导致线程的频繁创建与销毁。
  2. maximumPoolSize:线程池能够容纳同时执行的线程最大数,必须大于等于 1,如果与核心线程数设置相同代表固定大小线程池。
  3. keepAliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存资源。
  4. unit:keepAliveTime 的时间单位。
  5. workQueue:工作队列,当线程请求数大于等于 corePoolSize 时线程会进入阻塞队列。
  6. threadFactory:线程工厂,用来生产一组相同任务的线程。可以给线程命名,有利于分析错误。
  7. handler:拒绝策略,默认使用 AbortPolicy 丢弃任务并抛出异常,CallerRunsPolicy 表示重新尝试提交该任务,DiscardOldestPolicy 表示抛弃队列里等待最久的任务并把当前任务加入队列,DiscardPolicy 表示直接抛弃当前任务但不抛出异常。

1.3.12. 如何关闭线程池?

可以调用 shutdownshutdownNow 方法关闭线程池,原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法中断线程,无法响应中断的任务可能永远无法终止。

区别是 shutdownNow 首先将线程池的状态设为 STOP,然后尝试停止正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设为 SHUTDOWN,然后中断没有正在执行任务的线程。

通常调用 shutdown 来关闭线程池,如果任务不一定要执行完可调用 shutdownNow

1.3.13. 线程池的选择策略有什么?

可以从以下角度分析:①任务性质:CPU 密集型、IO 密集型和混合型。②任务优先级。③任务执行时间。④任务依赖性:是否依赖其他资源,如数据库连接。

性质不同的任务可用不同规模的线程池处理,CPU 密集型任务应配置尽可能小的线程,如配置 Ncpu+1 个线程的线程池。由于 IO 密集型任务线程并不是一直在执行任务,应配置尽可能多的线程,如 2*Ncpu。混合型的任务,如果可以拆分,将其拆分为一个 CPU 密集型任务和一个 IO 密集型任务,只要两个任务执行的时间相差不大那么分解后的吞吐量将高于串行执行的吞吐量,如果相差太大则没必要分解。

优先级不同的任务可以使用优先级队列 PriorityBlockingQueue 处理。

执行时间不同的任务可以交给不同规模的线程池处理,或者使用优先级队列让执行时间短的任务先执行。

依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越长 CPU 空闲的时间就越长,因此线程数应该尽可能地设置大一些,提高 CPU 的利用率。

建议使用有界队列,能增加系统的稳定性和预警能力,可以根据需要设置的稍微大一些。

1.3.14. 阻塞队列有哪些选择?

阻塞队列支持阻塞插入和移除,当队列满时,阻塞插入元素的线程直到队列不满。当队列为空时,获取元素的线程会被阻塞直到队列非空。阻塞队列常用于生产者和消费者的场景,阻塞队列就是生产者用来存放元素,消费者用来获取元素的容器。

  • ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线程最后才访问队列。
  • LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最大长度为 Integer 最大值。
  • PriorityBlockingQueue,支持优先级的无界阻塞队列,默认情况下元素按照升序排序。可自定义 compareTo 方法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺序。
  • DelayQueue,支持延时获取元素的无界阻塞队列,使用优先级队列实现。创建元素时可以指定多久才能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适用于缓存和定时调度。
  • SynchronousQueue,不存储元素的阻塞队列,每一个 put 必须等待一个 take。默认使用非公平策略,也支持公平策略,适用于传递性场景,吞吐量高。
  • LinkedTransferQueue,链表组成的无界阻塞队列,相对于其他阻塞队列多了 tryTransfertransfer 方法。transfer方法:如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。tryTransfer 方法用来试探生产者传入的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和 transfer 的区别是无论消费者是否消费都会立即返回。
  • LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插入和移出元素,多线程同时入队时减少了竞争。

实现原理

使用通知模式实现,生产者往满的队列里添加元素时会阻塞,当消费者消费后,会通知生产者当前队列可用。当往队列里插入一个元素,如果队列不可用,阻塞生产者主要通过 LockSupport 的 park 方法实现,不同操作系统中实现方式不同,在 Linux 下使用的是系统方法 pthread_cond_wait 实现。

1.3.15. 谈一谈 ThreadLocal

ThreadLoacl 是线程共享变量,主要用于一个线程内跨类、方法传递数据。ThreadLoacl 有一个静态内部类 ThreadLocalMap,其 Key 是 ThreadLocal 对象,值是 Entry 对象,Entry 中只有一个 Object 类的 vaule 值。ThreadLocal 是线程共享的,但 ThreadLocalMap 是每个线程私有的。ThreadLocal 主要有 set、get 和 remove 三个方法。

  • set 方法:首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传入的参数。如果 map 不存在就通过 createMap 方法为当前线程创建一个 ThreadLocalMap 对象再设置值。
  • get 方法:首先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。如果 e 不存在或者 map 不存在,就调用 setInitialValue 方法先为当前线程创建一个 ThreadLocalMap 对象然后返回默认的初始值 null。
  • remove 方法:首先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除 ThreadLocal 这个 key 及其对应的 value 值的联系。
  • 存在的问题:线程复用会产生脏数据,由于线程池会重用 Thread 对象,因此与 Thread 绑定的 ThreadLocal 也会被重用。如果没有调用 remove 清理与线程相关的 ThreadLocal 信息,那么假如下一个线程没有调用 set 设置初始值就可能 get 到重用的线程信息。ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引用,但 Entry 的 value 是强引用,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调用 remove 方法进行清理操作。

1.4. JUC

1.4.1. ReadWriteLock独写锁

ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。


class Cache<K, V> {
    final Map<K, V> m = new HashMap<>();
    final ReadWriteLock rwl = new ReentrantReadWriteLock();
    final Lock r = rwl.readLock();
    final Lock w = rwl.writeLock();

    V get(K key) {
        V v = null;
        //读缓存
        r.lock();
        try {
            v = m.get(key);
        } finally {
            r.unlock();
        }
        //缓存中存在,返回
        if (v != null) {
            return v;
        }
        //缓存中不存在,查询数据库
        w.lock();
        try {
            //再次验证
            //其他线程可能已经查询过数据库
            v = m.get(key);
            if (v == null) {
                //查询数据库
                //v = 省略代码无数
                m.put(key, v);
            }
        } finally {
            w.unlock();
        }
        return v;
    }
}

1.4.2. StampedLock

StampedLock 支持三种模式,分别是:写锁、悲观读锁和乐观读。

final StampedLock sl = new StampedLock();
// 获取/释放悲观读锁示意代码
long stamp = sl.readLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockRead(stamp);
}
// 获取/释放写锁示意代码
long stamp = sl.writeLock();
try {
  //省略业务相关代码
} finally {
  sl.unlockWrite(stamp);
}

最佳实践

class Point {
  private int x, y;
  final StampedLock sl = new StampedLock();
  //计算到原点的距离  
  int distanceFromOrigin() {
    // 乐观读
    long stamp = sl.tryOptimisticRead();
    // 读入局部变量,
    // 读的过程数据可能被修改
    int curX = x, curY = y;
    //判断执行读操作期间,
    //是否存在写操作,如果存在,
    //则sl.validate返回false
    if (!sl.validate(stamp)){
      // 升级为悲观读锁
      stamp = sl.readLock();
      try {
        curX = x;
        curY = y;
      } finally {
        //释放悲观读锁
        sl.unlockRead(stamp);
      }
    }
    return Math.sqrt(curX * curX + curY * curY);
  }
}

1.4.3. CountDownLatch、CyclicBarrier

  • CountDownLatch 是基于执行时间的同步类,允许一个或多个线程等待其他线程完成操作,构造方法接收一个 int 参数作为计数器,如果要等待 n 个点就传入 n。每次调用 countDown 方法时计数器减 1,await 方阻塞当前线程直到计数器变为0,由于 countDown 方法可用在任何地方,所以 n 个点既可以是 n 个线程也可以是一个线程里的 n 个执行步骤。
  • CyclicBarrier 循环屏障是基于同步到达某个点的信号量触发机制,作用是让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障才会解除。构造方法中的参数表示拦截线程数量,每个线程调用 await 方法告诉 CyclicBarrier 自己已到达屏障,然后被阻塞。还支持在构造方法中传入一个 Runnable 任务,当线程到达屏障时会优先执行该任务。适用于多线程计算数据,最后合并计算结果的应用场景。
    • CountDownLacth 的计数器只能用一次,而 CyclicBarrier 的计数器可使用 reset 方法重置,所以 CyclicBarrier 能处理更为复杂的业务场景,例如计算错误时可用重置计数器重新计算。
  • Semaphore:信号量用来控制同时访问特定资源的线程数量,通过协调各个线程以保证合理使用公共资源。信号量可以用于流量控制,特别是公共资源有限的应用场景,比如数据库连接。Semaphore 的构造方法参数接收一个 int 值,表示可用的许可数量即最大并发数。使用 acquire 方法获得一个许可证,使用 release 方法归还许可,还可以用 tryAcquire 尝试获得许可。

CountDownLatch 使用

// 创建2个线程的线程池
Executor executor =  Executors.newFixedThreadPool(2);
while(存在未对账订单){
  // 计数器初始化为2
  CountDownLatch latch =  new CountDownLatch(2);
  // 查询未对账订单
  executor.execute(()-> {
    pos = getPOrders();
    latch.countDown();
  });
  // 查询派送单
  executor.execute(()-> {
    dos = getDOrders();
    latch.countDown();
  });
  // 等待两个查询操作结束
  latch.await();
  // 执行对账操作
  diff = check(pos, dos);
  // 差异写入差异库
  save(diff);
}

CyclicBarrier使用


// 订单队列
Vector<P> pos;
// 派送单队列
Vector<D> dos;
// 执行回调的线程池 
Executor executor =  Executors.newFixedThreadPool(1);
// 满足条件后执行check
final CyclicBarrier barrier =  new CyclicBarrier(2, ()->{ executor.execute(()->check()); });

void check(){
  P p = pos.remove(0);
  D d = dos.remove(0);
  // 执行对账操作
  diff = check(p, d);
  // 差异写入差异库
  save(diff);
}

void checkAll(){
  // 循环查询订单库
  Thread T1 = new Thread(()->{
    while(存在未对账订单){
      // 查询订单库
      pos.add(getPOrders());
      // 等待
      barrier.await();
    }
  });
  T1.start();  
  // 循环查询运单库
  Thread T2 = new Thread(()->{
    while(存在未对账订单){
      // 查询运单库
      dos.add(getDOrders());
      // 等待
      barrier.await();
    }
  });
  T2.start();
}

Semaphore

public class ObjPool<T, R> {
    final List<T> pool;
    // 用信号量实现限流器
    final Semaphore sem;
    // 构造函数
    ObjPool(int size, T t) {
        pool = new Vector<T>();
        for (int i = 0; i < size; i++) {
            pool.add(t);
        }
        sem = new Semaphore(size);
    }
    // 利用对象池的对象,调用func
    R exec(Function<T, R> func) throws InterruptedException {
        T t = null;
        sem.acquire();
        try {
            t = pool.remove(0);
            return func.apply(t);
        } finally {
            pool.add(t);
            sem.release();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        // 创建对象池
        ObjPool<Long, String> pool = new ObjPool<>(10, (long) 2);
        // 通过对象池获取t,之后执行
        pool.exec(t -> {
            System.out.println(t);
            return t.toString();
        });

    }
}

results matching ""

    No results matching ""