- [1 AQS 简单介绍](#1-aqs-简单介绍)- [2 AQS 原理](#2-aqs-原理) - [2.1 AQS 原理概览](#21-aqs-原理概览) - [2.2 AQS ..._,ssh,java,util.concurrent,原子,计数器,进程树">
赞
踩
- 点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
-
- <!-- TOC -->
-
- - [1 AQS 简单介绍](#1-aqs-简单介绍)
- - [2 AQS 原理](#2-aqs-原理)
- - [2.1 AQS 原理概览](#21-aqs-原理概览)
- - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式)
- - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式)
- - [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问)
- - [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器)
- - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法)
- - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例)
- - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足)
- - [4.4 CountDownLatch 相常见面试题:](#44-countdownlatch-相常见面试题)
- - [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏)
- - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景)
- - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例)
- - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析)
- - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别)
- - [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock)
- - [参考](#参考)
- - [公众号](#公众号)
-
- <!-- /TOC -->
-
- > 常见问题:AQS 原理?;CountDownLatch 和 CyclicBarrier 了解吗,两者的区别是什么?用过 Semaphore 吗?
-
- ### 1 AQS 简单介绍
-
- AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面。
-
- ![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/AQS.png)
-
- AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。
-
- ### 2 AQS 原理
-
- > 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于 AQS 原理的理解”。下面给大家一个示例供大家参考,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
-
- 下面大部分内容其实在 AQS 类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
-
- #### 2.1 AQS 原理概览
-
- **AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
-
- > CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
-
- 看个 AQS(AbstractQueuedSynchronizer)原理图:
-
- ![enter image description here](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/CLH.png)
-
- AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用 CAS 对该同步状态进行原子操作实现对其值的修改。
-
- ```java
- private volatile int state;//共享变量,使用volatile修饰保证线程可见性
- ```
-
- 状态信息通过 protected 类型的`getState`,`setState`,`compareAndSetState`进行操作
-
- ```java
- //返回同步状态的当前值
- protected final int getState() {
- return state;
- }
- // 设置同步状态的值
- protected final void setState(int newState) {
- state = newState;
- }
- //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
- protected final boolean compareAndSetState(int expect, int update) {
- return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
- }
- ```
-
- #### 2.2 AQS 对资源的共享方式
-
- **AQS 定义两种资源共享方式**
-
- **1)Exclusive**(独占)
-
- 只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:
-
- - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- - 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
-
- > 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2,这是一篇很不错文章,推荐阅读。
-
- **下面来看 ReentrantLock 中相关的源代码:**
-
- ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。
-
- ```java
- /** Synchronizer providing all implementation mechanics */
- private final Sync sync;
- public ReentrantLock() {
- // 默认非公平锁
- sync = new NonfairSync();
- }
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
- ```
-
- ReentrantLock 中公平锁的 `lock` 方法
-
- ```java
- static final class FairSync extends Sync {
- final void lock() {
- acquire(1);
- }
- // AbstractQueuedSynchronizer.acquire(int arg)
- public final void acquire(int arg) {
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
- protected final boolean tryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- if (c == 0) {
- // 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
- if (!hasQueuedPredecessors() &&
- compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0)
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
- }
- ```
-
- 非公平锁的 lock 方法:
-
- ```java
- static final class NonfairSync extends Sync {
- final void lock() {
- // 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
- if (compareAndSetState(0, 1))
- setExclusiveOwnerThread(Thread.currentThread());
- else
- acquire(1);
- }
- // AbstractQueuedSynchronizer.acquire(int arg)
- public final void acquire(int arg) {
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
- protected final boolean tryAcquire(int acquires) {
- return nonfairTryAcquire(acquires);
- }
- }
- /**
- * Performs non-fair tryLock. tryAcquire is implemented in
- * subclasses, but both need nonfair try for trylock method.
- */
- final boolean nonfairTryAcquire(int acquires) {
- final Thread current = Thread.currentThread();
- int c = getState();
- if (c == 0) {
- // 这里没有对阻塞队列进行判断
- if (compareAndSetState(0, acquires)) {
- setExclusiveOwnerThread(current);
- return true;
- }
- }
- else if (current == getExclusiveOwnerThread()) {
- int nextc = c + acquires;
- if (nextc < 0) // overflow
- throw new Error("Maximum lock count exceeded");
- setState(nextc);
- return true;
- }
- return false;
- }
- ```
-
- 总结:公平锁和非公平锁只有两处不同:
-
- 1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
-
- 公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
-
- 相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
-
- **2)Share**(共享)
-
- 多个线程可同时执行,如 Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
-
- ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。
-
- 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在上层已经帮我们实现好了。
-
- #### 2.3 AQS 底层使用了模板方法模式
-
- 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
-
- 1. 使用者继承 AbstractQueuedSynchronizer 并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
- 2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
-
- 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用,下面简单的给大家介绍一下模板方法模式,模板方法模式是一个很容易理解的设计模式之一。
-
- > 模板方法模式是基于”继承“的,主要是为了在不改变模板结构的前提下在子类中重新定义模板中的内容以实现复用代码。举个很简单的例子假如我们要去一个地方的步骤是:购票`buyTicket()`->安检`securityCheck()`->乘坐某某工具回家`ride()`->到达目的地`arrive()`。我们可能乘坐不同的交通工具回家比如飞机或者火车,所以除了`ride()`方法,其他方法的实现几乎相同。我们可以定义一个包含了这些方法的抽象类,然后用户根据自己的需要继承该抽象类然后修改 `ride()`方法。
-
- **AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的模板方法:**
-
- ```java
- isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
-
- ```
-
- 默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS 类中的其他方法都是 final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
-
- 以 ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。
-
- 再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await()函数返回,继续后余动作。
-
- 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
-
- 推荐两篇 AQS 原理和相关源码分析的文章:
-
- - http://www.cnblogs.com/waterystone/p/4920797.html
- - https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
-
- ### 3 Semaphore(信号量)-允许多个线程同时访问
-
- **synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** 示例代码如下:
-
- ```java
- /**
- *
- * @author Snailclimb
- * @date 2018年9月30日
- * @Description: 需要一次性拿一个许可的情况
- */
- public class SemaphoreExample1 {
- // 请求的数量
- private static final int threadCount = 550;
-
- public static void main(String[] args) throws InterruptedException {
- // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
- ExecutorService threadPool = Executors.newFixedThreadPool(300);
- // 一次只能允许执行的线程数量。
- final Semaphore semaphore = new Semaphore(20);
-
- for (int i = 0; i < threadCount; i++) {
- final int threadnum = i;
- threadPool.execute(() -> {// Lambda 表达式的运用
- try {
- semaphore.acquire();// 获取一个许可,所以可运行线程数量为20/1=20
- test(threadnum);
- semaphore.release();// 释放一个许可
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
-
- });
- }
- threadPool.shutdown();
- System.out.println("finish");
- }
-
- public static void test(int threadnum) throws InterruptedException {
- Thread.sleep(1000);// 模拟请求的耗时操作
- System.out.println("threadnum:" + threadnum);
- Thread.sleep(1000);// 模拟请求的耗时操作
- }
- }
- ```
-
- 执行 `acquire` 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个 `release` 方法增加一个许可证,这可能会释放一个阻塞的 acquire 方法。然而,其实并没有实际的许可证这个对象,Semaphore 只是维持了一个可获得许可证的数量。 Semaphore 经常用于限制获取某种资源的线程数量。
-
- 当然一次也可以一次拿取和释放多个许可,不过一般没有必要这样做:
-
- ```java
- semaphore.acquire(5);// 获取5个许可,所以可运行线程数量为20/5=4
- test(threadnum);
- semaphore.release(5);// 获取5个许可,所以可运行线程数量为20/5=4
- ```
-
- 除了 `acquire`方法之外,另一个比较常用的与之对应的方法是`tryAcquire`方法,该方法如果获取不到许可就立即返回 false。
-
- Semaphore 有两种模式,公平模式和非公平模式。
-
- - **公平模式:** 调用 acquire 的顺序就是获取许可证的顺序,遵循 FIFO;
- - **非公平模式:** 抢占式的。
-
- **Semaphore 对应的两个构造方法如下:**
-
- ```java
- public Semaphore(int permits) {
- sync = new NonfairSync(permits);
- }
-
- public Semaphore(int permits, boolean fair) {
- sync = fair ? new FairSync(permits) : new NonfairSync(permits);
- }
- ```
-
- **这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。**
-
- 由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下面这篇文章:
-
- - https://blog.csdn.net/qq_19431333/article/details/70212663
-
- ### 4 CountDownLatch (倒计时器)
-
- CountDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完后再执行。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。
-
- #### 4.1 CountDownLatch 的三种典型用法
-
- ① 某一线程在开始运行前等待 n 个线程执行完毕。将 CountDownLatch 的计数器初始化为 n :`new CountDownLatch(n)`,每当一个任务线程执行完毕,就将计数器减 1 `countdownlatch.countDown()`,当计数器的值变为 0 时,在`CountDownLatch上 await()` 的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
-
- ② 实现多个线程开始执行任务的最大并行性。注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 `CountDownLatch` 对象,将其计数器初始化为 1 :`new CountDownLatch(1)`,多个线程在开始执行任务前首先 `coundownlatch.await()`,当主线程调用 countDown() 时,计数器变为 0,多个线程同时被唤醒。
-
- ③ 死锁检测:一个非常方便的使用场景是,你可以使用 n 个线程访问共享资源,在每次测试阶段的线程数目是不同的,并尝试产生死锁。
-
- #### 4.2 CountDownLatch 的使用示例
-
- ```java
- /**
- *
- * @author SnailClimb
- * @date 2018年10月1日
- * @Description: CountDownLatch 使用方法示例
- */
- public class CountDownLatchExample1 {
- // 请求的数量
- private static final int threadCount = 550;
-
- public static void main(String[] args) throws InterruptedException {
- // 创建一个具有固定线程数量的线程池对象(如果这里线程池的线程数量给太少的话你会发现执行的很慢)
- ExecutorService threadPool = Executors.newFixedThreadPool(300);
- final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
- for (int i = 0; i < threadCount; i++) {
- final int threadnum = i;
- threadPool.execute(() -> {// Lambda 表达式的运用
- try {
- test(threadnum);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- countDownLatch.countDown();// 表示一个请求已经被完成
- }
-
- });
- }
- countDownLatch.await();
- threadPool.shutdown();
- System.out.println("finish");
- }
-
- public static void test(int threadnum) throws InterruptedException {
- Thread.sleep(1000);// 模拟请求的耗时操作
- System.out.println("threadnum:" + threadnum);
- Thread.sleep(1000);// 模拟请求的耗时操作
- }
- }
-
- ```
-
- 上面的代码中,我们定义了请求的数量为 550,当这 550 个请求被处理完成之后,才会执行`System.out.println("finish");`。
-
- 与 CountDownLatch 的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用 CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。
-
- 其他 N 个线程必须引用闭锁对象,因为他们需要通知 CountDownLatch 对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 await()方法,恢复执行自己的任务。
-
- #### 4.3 CountDownLatch 的不足
-
- CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。
-
- #### 4.4 CountDownLatch 相常见面试题:
-
- 解释一下 CountDownLatch 概念?
-
- CountDownLatch 和 CyclicBarrier 的不同之处?
-
- 给出一些 CountDownLatch 使用的例子?
-
- CountDownLatch 类中主要的方法?
-
- ### 5 CyclicBarrier(循环栅栏)
-
- CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。
-
- CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
-
- 再来看一下它的构造函数:
-
- ```java
- public CyclicBarrier(int parties) {
- this(parties, null);
- }
-
- public CyclicBarrier(int parties, Runnable barrierAction) {
- if (parties <= 0) throw new IllegalArgumentException();
- this.parties = parties;
- this.count = parties;
- this.barrierCommand = barrierAction;
- }
- ```
-
- 其中,parties 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。
-
- #### 5.1 CyclicBarrier 的应用场景
-
- CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的应用场景。比如我们用一个 Excel 保存了用户所有银行流水,每个 Sheet 保存一个帐户近一年的每笔银行流水,现在需要统计用户的日均银行流水,先用多线程处理每个 sheet 里的银行流水,都执行完之后,得到每个 sheet 的日均银行流水,最后,再用 barrierAction 用这些线程的计算结果,计算出整个 Excel 的日均银行流水。
-
- #### 5.2 CyclicBarrier 的使用示例
-
- 示例 1:
-
- ```java
- /**
- *
- * @author Snailclimb
- * @date 2018年10月1日
- * @Description: 测试 CyclicBarrier 类中带参数的 await() 方法
- */
- public class CyclicBarrierExample2 {
- // 请求的数量
- private static final int threadCount = 550;
- // 需要同步的线程数量
- private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
-
- public static void main(String[] args) throws InterruptedException {
- // 创建线程池
- ExecutorService threadPool = Executors.newFixedThreadPool(10);
-
- for (int i = 0; i < threadCount; i++) {
- final int threadNum = i;
- Thread.sleep(1000);
- threadPool.execute(() -> {
- try {
- test(threadNum);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (BrokenBarrierException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- });
- }
- threadPool.shutdown();
- }
-
- public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
- System.out.println("threadnum:" + threadnum + "is ready");
- try {
- /**等待60秒,保证子线程完全执行结束*/
- cyclicBarrier.await(60, TimeUnit.SECONDS);
- } catch (Exception e) {
- System.out.println("-----CyclicBarrierException------");
- }
- System.out.println("threadnum:" + threadnum + "is finish");
- }
-
- }
- ```
-
- 运行结果,如下:
-
- ```
- threadnum:0is ready
- threadnum:1is ready
- threadnum:2is ready
- threadnum:3is ready
- threadnum:4is ready
- threadnum:4is finish
- threadnum:0is finish
- threadnum:1is finish
- threadnum:2is finish
- threadnum:3is finish
- threadnum:5is ready
- threadnum:6is ready
- threadnum:7is ready
- threadnum:8is ready
- threadnum:9is ready
- threadnum:9is finish
- threadnum:5is finish
- threadnum:8is finish
- threadnum:7is finish
- threadnum:6is finish
- ......
- ```
-
- 可以看到当线程数量也就是请求数量达到我们定义的 5 个的时候, `await`方法之后的方法才被执行。
-
- 另外,CyclicBarrier 还提供一个更高级的构造函数`CyclicBarrier(int parties, Runnable barrierAction)`,用于在线程到达屏障时,优先执行`barrierAction`,方便处理更复杂的业务场景。示例代码如下:
-
- ```java
- /**
- *
- * @author SnailClimb
- * @date 2018年10月1日
- * @Description: 新建 CyclicBarrier 的时候指定一个 Runnable
- */
- public class CyclicBarrierExample3 {
- // 请求的数量
- private static final int threadCount = 550;
- // 需要同步的线程数量
- private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
- System.out.println("------当线程数达到之后,优先执行------");
- });
-
- public static void main(String[] args) throws InterruptedException {
- // 创建线程池
- ExecutorService threadPool = Executors.newFixedThreadPool(10);
-
- for (int i = 0; i < threadCount; i++) {
- final int threadNum = i;
- Thread.sleep(1000);
- threadPool.execute(() -> {
- try {
- test(threadNum);
- } catch (InterruptedException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (BrokenBarrierException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- }
- });
- }
- threadPool.shutdown();
- }
-
- public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
- System.out.println("threadnum:" + threadnum + "is ready");
- cyclicBarrier.await();
- System.out.println("threadnum:" + threadnum + "is finish");
- }
-
- }
- ```
-
- 运行结果,如下:
-
- ```
- threadnum:0is ready
- threadnum:1is ready
- threadnum:2is ready
- threadnum:3is ready
- threadnum:4is ready
- ------当线程数达到之后,优先执行------
- threadnum:4is finish
- threadnum:0is finish
- threadnum:2is finish
- threadnum:1is finish
- threadnum:3is finish
- threadnum:5is ready
- threadnum:6is ready
- threadnum:7is ready
- threadnum:8is ready
- threadnum:9is ready
- ------当线程数达到之后,优先执行------
- threadnum:9is finish
- threadnum:5is finish
- threadnum:6is finish
- threadnum:8is finish
- threadnum:7is finish
- ......
- ```
-
- #### 5.3 `CyclicBarrier`源码分析
-
- 当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是`dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties 的值时,栅栏才会打开,线程才得以通过执行。
-
- ```java
- public int await() throws InterruptedException, BrokenBarrierException {
- try {
- return dowait(false, 0L);
- } catch (TimeoutException toe) {
- throw new Error(toe); // cannot happen
- }
- }
- ```
-
- `dowait(false, 0L)`:
-
- ```java
- // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。
- private int count;
- /**
- * Main barrier code, covering the various policies.
- */
- private int dowait(boolean timed, long nanos)
- throws InterruptedException, BrokenBarrierException,
- TimeoutException {
- final ReentrantLock lock = this.lock;
- // 锁住
- lock.lock();
- try {
- final Generation g = generation;
-
- if (g.broken)
- throw new BrokenBarrierException();
-
- // 如果线程中断了,抛出异常
- if (Thread.interrupted()) {
- breakBarrier();
- throw new InterruptedException();
- }
- // cout减1
- int index = --count;
- // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行await 方法之后的条件
- if (index == 0) { // tripped
- boolean ranAction = false;
- try {
- final Runnable command = barrierCommand;
- if (command != null)
- command.run();
- ranAction = true;
- // 将 count 重置为 parties 属性的初始化值
- // 唤醒之前等待的线程
- // 下一波执行开始
- nextGeneration();
- return 0;
- } finally {
- if (!ranAction)
- breakBarrier();
- }
- }
-
- // loop until tripped, broken, interrupted, or timed out
- for (;;) {
- try {
- if (!timed)
- trip.await();
- else if (nanos > 0L)
- nanos = trip.awaitNanos(nanos);
- } catch (InterruptedException ie) {
- if (g == generation && ! g.broken) {
- breakBarrier();
- throw ie;
- } else {
- // We're about to finish waiting even if we had not
- // been interrupted, so this interrupt is deemed to
- // "belong" to subsequent execution.
- Thread.currentThread().interrupt();
- }
- }
-
- if (g.broken)
- throw new BrokenBarrierException();
-
- if (g != generation)
- return index;
-
- if (timed && nanos <= 0L) {
- breakBarrier();
- throw new TimeoutException();
- }
- }
- } finally {
- lock.unlock();
- }
- }
-
- ```
-
- 总结:`CyclicBarrier` 内部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。
-
- #### 5.4 CyclicBarrier 和 CountDownLatch 的区别
-
- **下面这个是国外一个大佬的回答:**
-
- CountDownLatch 是计数器,只能使用一次,而 CyclicBarrier 的计数器提供 reset 功能,可以多次使用。但是我不那么认为它们之间的区别仅仅就是这么简单的一点。我们来从 jdk 作者设计的目的来看,javadoc 是这么描述它们的:
-
- > CountDownLatch: A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.(CountDownLatch: 一个或者多个线程,等待其他多个线程完成某件事情之后才能执行;)
- > CyclicBarrier : A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.(CyclicBarrier : 多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
-
- 对于 CountDownLatch 来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
-
- CountDownLatch 是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier 更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。
-
- ### 6 ReentrantLock 和 ReentrantReadWriteLock
-
- ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多做讲解。另外,需要注意的是:读写锁 ReentrantReadWriteLock 可以保证多个线程可以同时读,所以在读操作远大于写操作的时候,读写锁就非常有用了。
-
- ### 参考
-
- - https://juejin.im/post/5ae755256fb9a07ac3634067
- - https://blog.csdn.net/u010185262/article/details/54692886
- - https://blog.csdn.net/tolcf/article/details/50925145?utm_source=blogxgwz0
- 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
-
- > 个人觉得这一节掌握基本的使用即可!
-
- <!-- TOC -->
-
- - [1 Atomic 原子类介绍](#1-atomic-原子类介绍)
- - [2 基本类型原子类](#2-基本类型原子类)
- - [2.1 基本类型原子类介绍](#21-基本类型原子类介绍)
- - [2.2 AtomicInteger 常见方法使用](#22-atomicinteger-常见方法使用)
- - [2.3 基本数据类型原子类的优势](#23-基本数据类型原子类的优势)
- - [2.4 AtomicInteger 线程安全原理简单分析](#24-atomicinteger-线程安全原理简单分析)
- - [3 数组类型原子类](#3-数组类型原子类)
- - [3.1 数组类型原子类介绍](#31-数组类型原子类介绍)
- - [3.2 AtomicIntegerArray 常见方法使用](#32-atomicintegerarray-常见方法使用)
- - [4 引用类型原子类](#4-引用类型原子类)
- - [4.1 引用类型原子类介绍](#41--引用类型原子类介绍)
- - [4.2 AtomicReference 类使用示例](#42-atomicreference-类使用示例)
- - [4.3 AtomicStampedReference 类使用示例](#43-atomicstampedreference-类使用示例)
- - [4.4 AtomicMarkableReference 类使用示例](#44-atomicmarkablereference-类使用示例)
- - [5 对象的属性修改类型原子类](#5-对象的属性修改类型原子类)
- - [5.1 对象的属性修改类型原子类介绍](#51-对象的属性修改类型原子类介绍)
- - [5.2 AtomicIntegerFieldUpdater 类使用示例](#52-atomicintegerfieldupdater-类使用示例)
-
- <!-- /TOC -->
-
- ### 1 Atomic 原子类介绍
-
- Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
-
- 所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
-
- 并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
-
- ![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png)
-
- 根据操作的数据类型,可以将JUC包中的原子类分为4类
-
- **基本类型**
-
- 使用原子的方式更新基本类型
-
- - AtomicInteger:整型原子类
- - AtomicLong:长整型原子类
- - AtomicBoolean :布尔型原子类
-
- **数组类型**
-
- 使用原子的方式更新数组里的某个元素
-
-
- - AtomicIntegerArray:整型数组原子类
- - AtomicLongArray:长整型数组原子类
- - AtomicReferenceArray :引用类型数组原子类
-
- **引用类型**
-
- - AtomicReference:引用类型原子类
- - AtomicReferenceFieldUpdater:原子更新引用类型里的字段
- - AtomicMarkableReference :原子更新带有标记位的引用类型
-
- **对象的属性修改类型**
-
- - AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- - AtomicLongFieldUpdater:原子更新长整型字段的更新器
- - AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- - AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,也可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
-
- **CAS ABA 问题**
- - 描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功。
- - 例子描述(可能不太合适,但好理解): 年初,现金为零,然后通过正常劳动赚了三百万,之后正常消费了(比如买房子)三百万。年末,虽然现金零收入(可能变成其他形式了),但是赚了钱是事实,还是得交税的!
- - 代码例子(以``` AtomicInteger ```为例)
-
- ```java
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class AtomicIntegerDefectDemo {
- public static void main(String[] args) {
- defectOfABA();
- }
-
- static void defectOfABA() {
- final AtomicInteger atomicInteger = new AtomicInteger(1);
-
- Thread coreThread = new Thread(
- () -> {
- final int currentValue = atomicInteger.get();
- System.out.println(Thread.currentThread().getName() + " ------ currentValue=" + currentValue);
-
- // 这段目的:模拟处理其他业务花费的时间
- try {
- Thread.sleep(300);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- boolean casResult = atomicInteger.compareAndSet(1, 2);
- System.out.println(Thread.currentThread().getName()
- + " ------ currentValue=" + currentValue
- + ", finalValue=" + atomicInteger.get()
- + ", compareAndSet Result=" + casResult);
- }
- );
- coreThread.start();
-
- // 这段目的:为了让 coreThread 线程先跑起来
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
-
- Thread amateurThread = new Thread(
- () -> {
- int currentValue = atomicInteger.get();
- boolean casResult = atomicInteger.compareAndSet(1, 2);
- System.out.println(Thread.currentThread().getName()
- + " ------ currentValue=" + currentValue
- + ", finalValue=" + atomicInteger.get()
- + ", compareAndSet Result=" + casResult);
-
- currentValue = atomicInteger.get();
- casResult = atomicInteger.compareAndSet(2, 1);
- System.out.println(Thread.currentThread().getName()
- + " ------ currentValue=" + currentValue
- + ", finalValue=" + atomicInteger.get()
- + ", compareAndSet Result=" + casResult);
- }
- );
- amateurThread.start();
- }
- }
- ```
-
- 输出内容如下:
-
- ```
- Thread-0 ------ currentValue=1
- Thread-1 ------ currentValue=1, finalValue=2, compareAndSet Result=true
- Thread-1 ------ currentValue=2, finalValue=1, compareAndSet Result=true
- Thread-0 ------ currentValue=1, finalValue=2, compareAndSet Result=true
- ```
-
- 下面我们来详细介绍一下这些原子类。
-
- ### 2 基本类型原子类
-
- #### 2.1 基本类型原子类介绍
-
- 使用原子的方式更新基本类型
-
- - AtomicInteger:整型原子类
- - AtomicLong:长整型原子类
- - AtomicBoolean :布尔型原子类
-
- 上面三个类提供的方法几乎相同,所以我们这里以 AtomicInteger 为例子来介绍。
-
- **AtomicInteger 类常用方法**
-
- ```java
- public final int get() //获取当前的值
- public final int getAndSet(int newValue)//获取当前的值,并设置新的值
- public final int getAndIncrement()//获取当前的值,并自增
- public final int getAndDecrement() //获取当前的值,并自减
- public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
- boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
- public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- ```
-
- #### 2.2 AtomicInteger 常见方法使用
-
- ```java
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class AtomicIntegerTest {
-
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- int temvalue = 0;
- AtomicInteger i = new AtomicInteger(0);
- temvalue = i.getAndSet(3);
- System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:0; i:3
- temvalue = i.getAndIncrement();
- System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:3; i:4
- temvalue = i.getAndAdd(5);
- System.out.println("temvalue:" + temvalue + "; i:" + i);//temvalue:4; i:9
- }
-
- }
- ```
-
- #### 2.3 基本数据类型原子类的优势
-
- 通过一个简单例子带大家看一下基本数据类型原子类的优势
-
- **①多线程环境不使用原子类保证线程安全(基本数据类型)**
-
- ```java
- class Test {
- private volatile int count = 0;
- //若要线程安全执行执行count++,需要加锁
- public synchronized void increment() {
- count++;
- }
-
- public int getCount() {
- return count;
- }
- }
- ```
- **②多线程环境使用原子类保证线程安全(基本数据类型)**
-
- ```java
- class Test2 {
- private AtomicInteger count = new AtomicInteger();
-
- public void increment() {
- count.incrementAndGet();
- }
- //使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
- public int getCount() {
- return count.get();
- }
- }
-
- ```
- #### 2.4 AtomicInteger 线程安全原理简单分析
-
- AtomicInteger 类的部分源码:
-
- ```java
- // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
-
- static {
- try {
- valueOffset = unsafe.objectFieldOffset
- (AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- private volatile int value;
- ```
-
- AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
-
- CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
-
-
- ### 3 数组类型原子类
-
- #### 3.1 数组类型原子类介绍
-
- 使用原子的方式更新数组里的某个元素
-
-
- - AtomicIntegerArray:整形数组原子类
- - AtomicLongArray:长整形数组原子类
- - AtomicReferenceArray :引用类型数组原子类
-
- 上面三个类提供的方法几乎相同,所以我们这里以 AtomicIntegerArray 为例子来介绍。
-
- **AtomicIntegerArray 类常用方法**
-
- ```java
- public final int get(int i) //获取 index=i 位置元素的值
- public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
- public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
- public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
- public final int getAndAdd(int delta) //获取 index=i 位置元素的值,并加上预期的值
- boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
- public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- ```
- #### 3.2 AtomicIntegerArray 常见方法使用
-
- ```java
-
- import java.util.concurrent.atomic.AtomicIntegerArray;
-
- public class AtomicIntegerArrayTest {
-
- public static void main(String[] args) {
- // TODO Auto-generated method stub
- int temvalue = 0;
- int[] nums = { 1, 2, 3, 4, 5, 6 };
- AtomicIntegerArray i = new AtomicIntegerArray(nums);
- for (int j = 0; j < nums.length; j++) {
- System.out.println(i.get(j));
- }
- temvalue = i.getAndSet(0, 2);
- System.out.println("temvalue:" + temvalue + "; i:" + i);
- temvalue = i.getAndIncrement(0);
- System.out.println("temvalue:" + temvalue + "; i:" + i);
- temvalue = i.getAndAdd(0, 5);
- System.out.println("temvalue:" + temvalue + "; i:" + i);
- }
-
- }
- ```
-
- ### 4 引用类型原子类
-
- #### 4.1 引用类型原子类介绍
-
- 基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用 引用类型原子类。
-
- - AtomicReference:引用类型原子类
- - AtomicStampedReference:原子更新引用类型里的字段原子类
- - AtomicMarkableReference :原子更新带有标记位的引用类型
-
- 上面三个类提供的方法几乎相同,所以我们这里以 AtomicReference 为例子来介绍。
-
- #### 4.2 AtomicReference 类使用示例
-
- ```java
- import java.util.concurrent.atomic.AtomicReference;
-
- public class AtomicReferenceTest {
-
- public static void main(String[] args) {
- AtomicReference<Person> ar = new AtomicReference<Person>();
- Person person = new Person("SnailClimb", 22);
- ar.set(person);
- Person updatePerson = new Person("Daisy", 20);
- ar.compareAndSet(person, updatePerson);
-
- System.out.println(ar.get().getName());
- System.out.println(ar.get().getAge());
- }
- }
-
- class Person {
- private String name;
- private int age;
-
- public Person(String name, int age) {
- super();
- this.name = name;
- this.age = age;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public int getAge() {
- return age;
- }
-
- public void setAge(int age) {
- this.age = age;
- }
-
- }
- ```
- 上述代码首先创建了一个 Person 对象,然后把 Person 对象设置进 AtomicReference 对象中,然后调用 compareAndSet 方法,该方法就是通过 CAS 操作设置 ar。如果 ar 的值为 person 的话,则将其设置为 updatePerson。实现原理与 AtomicInteger 类中的 compareAndSet 方法相同。运行上面的代码后的输出结果如下:
-
- ```
- Daisy
- 20
- ```
- #### 4.3 AtomicStampedReference 类使用示例
-
- ```java
- import java.util.concurrent.atomic.AtomicStampedReference;
-
- public class AtomicStampedReferenceDemo {
- public static void main(String[] args) {
- // 实例化、取当前值和 stamp 值
- final Integer initialRef = 0, initialStamp = 0;
- final AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(initialRef, initialStamp);
- System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());
-
- // compare and set
- final Integer newReference = 666, newStamp = 999;
- final boolean casResult = asr.compareAndSet(initialRef, newReference, initialStamp, newStamp);
- System.out.println("currentValue=" + asr.getReference()
- + ", currentStamp=" + asr.getStamp()
- + ", casResult=" + casResult);
-
- // 获取当前的值和当前的 stamp 值
- int[] arr = new int[1];
- final Integer currentValue = asr.get(arr);
- final int currentStamp = arr[0];
- System.out.println("currentValue=" + currentValue + ", currentStamp=" + currentStamp);
-
- // 单独设置 stamp 值
- final boolean attemptStampResult = asr.attemptStamp(newReference, 88);
- System.out.println("currentValue=" + asr.getReference()
- + ", currentStamp=" + asr.getStamp()
- + ", attemptStampResult=" + attemptStampResult);
-
- // 重新设置当前值和 stamp 值
- asr.set(initialRef, initialStamp);
- System.out.println("currentValue=" + asr.getReference() + ", currentStamp=" + asr.getStamp());
-
- // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
- // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
- // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
- // so is only rarely an appropriate alternative to compareAndSet."
- // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
- final boolean wCasResult = asr.weakCompareAndSet(initialRef, newReference, initialStamp, newStamp);
- System.out.println("currentValue=" + asr.getReference()
- + ", currentStamp=" + asr.getStamp()
- + ", wCasResult=" + wCasResult);
- }
- }
- ```
-
- 输出结果如下:
- ```
- currentValue=0, currentStamp=0
- currentValue=666, currentStamp=999, casResult=true
- currentValue=666, currentStamp=999
- currentValue=666, currentStamp=88, attemptStampResult=true
- currentValue=0, currentStamp=0
- currentValue=666, currentStamp=999, wCasResult=true
- ```
-
- #### 4.4 AtomicMarkableReference 类使用示例
-
- ``` java
- import java.util.concurrent.atomic.AtomicMarkableReference;
-
- public class AtomicMarkableReferenceDemo {
- public static void main(String[] args) {
- // 实例化、取当前值和 mark 值
- final Boolean initialRef = null, initialMark = false;
- final AtomicMarkableReference<Boolean> amr = new AtomicMarkableReference<>(initialRef, initialMark);
- System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());
-
- // compare and set
- final Boolean newReference1 = true, newMark1 = true;
- final boolean casResult = amr.compareAndSet(initialRef, newReference1, initialMark, newMark1);
- System.out.println("currentValue=" + amr.getReference()
- + ", currentMark=" + amr.isMarked()
- + ", casResult=" + casResult);
-
- // 获取当前的值和当前的 mark 值
- boolean[] arr = new boolean[1];
- final Boolean currentValue = amr.get(arr);
- final boolean currentMark = arr[0];
- System.out.println("currentValue=" + currentValue + ", currentMark=" + currentMark);
-
- // 单独设置 mark 值
- final boolean attemptMarkResult = amr.attemptMark(newReference1, false);
- System.out.println("currentValue=" + amr.getReference()
- + ", currentMark=" + amr.isMarked()
- + ", attemptMarkResult=" + attemptMarkResult);
-
- // 重新设置当前值和 mark 值
- amr.set(initialRef, initialMark);
- System.out.println("currentValue=" + amr.getReference() + ", currentMark=" + amr.isMarked());
-
- // [不推荐使用,除非搞清楚注释的意思了] weak compare and set
- // 困惑!weakCompareAndSet 这个方法最终还是调用 compareAndSet 方法。[版本: jdk-8u191]
- // 但是注释上写着 "May fail spuriously and does not provide ordering guarantees,
- // so is only rarely an appropriate alternative to compareAndSet."
- // todo 感觉有可能是 jvm 通过方法名在 native 方法里面做了转发
- final boolean wCasResult = amr.weakCompareAndSet(initialRef, newReference1, initialMark, newMark1);
- System.out.println("currentValue=" + amr.getReference()
- + ", currentMark=" + amr.isMarked()
- + ", wCasResult=" + wCasResult);
- }
- }
- ```
-
- 输出结果如下:
- ```
- currentValue=null, currentMark=false
- currentValue=true, currentMark=true, casResult=true
- currentValue=true, currentMark=true
- currentValue=true, currentMark=false, attemptMarkResult=true
- currentValue=null, currentMark=false
- currentValue=true, currentMark=true, wCasResult=true
- ```
-
- ### 5 对象的属性修改类型原子类
-
- #### 5.1 对象的属性修改类型原子类介绍
-
- 如果需要原子更新某个类里的某个字段时,需要用到对象的属性修改类型原子类。
-
- - AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- - AtomicLongFieldUpdater:原子更新长整形字段的更新器
- - AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
-
- 要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符。
-
- 上面三个类提供的方法几乎相同,所以我们这里以 `AtomicIntegerFieldUpdater`为例子来介绍。
-
- #### 5.2 AtomicIntegerFieldUpdater 类使用示例
-
- ```java
- import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
-
- public class AtomicIntegerFieldUpdaterTest {
- public static void main(String[] args) {
- AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
-
- User user = new User("Java", 22);
- System.out.println(a.getAndIncrement(user));// 22
- System.out.println(a.get(user));// 23
- }
- }
-
- class User {
- private String name;
- public volatile int age;
-
- public User(String name, int age) {
- super();
- this.name = name;
- this.age = age;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- public int getAge() {
- return age;
- }
-
- public void setAge(int age) {
- this.age = age;
- }
-
- }
- ```
-
- 输出结果:
-
- ```
- 22
- 23
- ```
-
- ## 公众号
-
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-
- **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
-
- **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
-
- ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- 点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
-
- <!-- TOC -->
-
- - [Java 并发进阶常见面试题总结](#java-并发进阶常见面试题总结)
- - [1. synchronized 关键字](#1-synchronized-关键字)
- - [1.1. 说一说自己对于 synchronized 关键字的了解](#11-说一说自己对于-synchronized-关键字的了解)
- - [1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗](#12-说说自己是怎么使用-synchronized-关键字在项目中用到了吗)
- - [1.3. 讲一下 synchronized 关键字的底层原理](#13-讲一下-synchronized-关键字的底层原理)
- - [1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗](#14-说说-jdk16-之后的synchronized-关键字底层做了哪些优化可以详细介绍一下这些优化吗)
- - [1.5. 谈谈 synchronized和ReentrantLock 的区别](#15-谈谈-synchronized和reentrantlock-的区别)
- - [2. volatile关键字](#2-volatile关键字)
- - [2.1. 讲一下Java内存模型](#21-讲一下java内存模型)
- - [2.2. 说说 synchronized 关键字和 volatile 关键字的区别](#22-说说-synchronized-关键字和-volatile-关键字的区别)
- - [3. ThreadLocal](#3-threadlocal)
- - [3.1. ThreadLocal简介](#31-threadlocal简介)
- - [3.2. ThreadLocal示例](#32-threadlocal示例)
- - [3.3. ThreadLocal原理](#33-threadlocal原理)
- - [3.4. ThreadLocal 内存泄露问题](#34-threadlocal-内存泄露问题)
- - [4. 线程池](#4-线程池)
- - [4.1. 为什么要用线程池?](#41-为什么要用线程池)
- - [4.2. 实现Runnable接口和Callable接口的区别](#42-实现runnable接口和callable接口的区别)
- - [4.3. 执行execute()方法和submit()方法的区别是什么呢?](#43-执行execute方法和submit方法的区别是什么呢)
- - [4.4. 如何创建线程池](#44-如何创建线程池)
- - [5. Atomic 原子类](#5-atomic-原子类)
- - [5.1. 介绍一下Atomic 原子类](#51-介绍一下atomic-原子类)
- - [5.2. JUC 包中的原子类是哪4类?](#52-juc-包中的原子类是哪4类)
- - [5.3. 讲讲 AtomicInteger 的使用](#53-讲讲-atomicinteger-的使用)
- - [5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理](#54-能不能给我简单介绍一下-atomicinteger-类的原理)
- - [6. AQS](#6-aqs)
- - [6.1. AQS 介绍](#61-aqs-介绍)
- - [6.2. AQS 原理分析](#62-aqs-原理分析)
- - [6.2.1. AQS 原理概览](#621-aqs-原理概览)
- - [6.2.2. AQS 对资源的共享方式](#622-aqs-对资源的共享方式)
- - [6.2.3. AQS底层使用了模板方法模式](#623-aqs底层使用了模板方法模式)
- - [6.3. AQS 组件总结](#63-aqs-组件总结)
- - [7 Reference](#7-reference)
-
- <!-- /TOC -->
-
- # Java 并发进阶常见面试题总结
-
- ## 1. synchronized 关键字
-
- ### 1.1. 说一说自己对于 synchronized 关键字的了解
-
- synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
-
- 另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
-
- ### 1.2. 说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
-
- **synchronized关键字最主要的三种使用方式:**
-
- - **修饰实例方法:** 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
- - **修饰静态方法:** 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
- - **修饰代码块:** 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
-
- **总结:** synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!
-
- 下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
-
- 面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
-
- **双重校验锁实现对象单例(线程安全)**
-
- ```java
- public class Singleton {
-
- private volatile static Singleton uniqueInstance;
-
- private Singleton() {
- }
-
- public static Singleton getUniqueInstance() {
- //先判断对象是否已经实例过,没有实例化过才进入加锁代码
- if (uniqueInstance == null) {
- //类对象加锁
- synchronized (Singleton.class) {
- if (uniqueInstance == null) {
- uniqueInstance = new Singleton();
- }
- }
- }
- return uniqueInstance;
- }
- }
- ```
- 另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
-
- uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
-
- 1. 为 uniqueInstance 分配内存空间
- 2. 初始化 uniqueInstance
- 3. 将 uniqueInstance 指向分配的内存地址
-
- 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
-
- 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
-
- ### 1.3. 讲一下 synchronized 关键字的底层原理
-
- **synchronized 关键字底层原理属于 JVM 层面。**
-
- **① synchronized 同步语句块的情况**
-
- ```java
- public class SynchronizedDemo {
- public void method() {
- synchronized (this) {
- System.out.println("synchronized 代码块");
- }
- }
- }
-
- ```
-
- 通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
-
- ![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理.png)
-
- 从上面我们可以看出:
-
- **synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
- **② synchronized 修饰方法的的情况**
-
- ```java
- public class SynchronizedDemo2 {
- public synchronized void method() {
- System.out.println("synchronized 方法");
- }
- }
-
- ```
-
- ![synchronized关键字原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/synchronized关键字原理2.png)
-
- synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
-
-
- ### 1.4. 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
-
- JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
-
- 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
- 关于这几种优化的详细信息可以查看笔主的这篇文章:<https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/Multithread/synchronized.md>
-
- ### 1.5. 谈谈 synchronized和ReentrantLock 的区别
-
-
- **① 两者都是可重入锁**
-
- 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
-
- **② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API**
-
- synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
-
- **③ ReentrantLock 比 synchronized 增加了一些高级功能**
-
- 相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
-
- - **ReentrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- - **ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
- - synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
-
- 如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。
-
- **④ 性能已不是选择标准**
-
- ## 2. volatile关键字
-
- ### 2.1. 讲一下Java内存模型
-
-
- 在 JDK1.2 之前,Java的内存模型实现总是从**主存**(即共享内存)读取变量,是不需要进行特别的注意的。而在当前的 Java 内存模型下,线程可以把变量保存**本地内存**(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成**数据的不一致**。
-
- ![数据不一致](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据不一致.png)
-
- 要解决这个问题,就需要把变量声明为**volatile**,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行读取。
-
- 说白了, **volatile** 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
-
- ![volatile关键字的可见性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/volatile关键字的可见性.png)
-
-
- ### 2.2. 说说 synchronized 关键字和 volatile 关键字的区别
-
- synchronized关键字和volatile关键字比较
-
- - **volatile关键字**是线程同步的**轻量级实现**,所以**volatile性能肯定比synchronized关键字要好**。但是**volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块**。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,**实际开发中使用 synchronized 关键字的场景还是更多一些**。
- - **多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞**
- - **volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。**
- - **volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。**
-
- ## 3. ThreadLocal
-
- ### 3.1. ThreadLocal简介
-
- 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
-
- **如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。**
-
- 再举个简单的例子:
-
- 比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来避免这两个线程竞争的。
-
- ### 3.2. ThreadLocal示例
-
- 相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。
-
- ```java
- import java.text.SimpleDateFormat;
- import java.util.Random;
-
- public class ThreadLocalExample implements Runnable{
-
- // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
- private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
-
- public static void main(String[] args) throws InterruptedException {
- ThreadLocalExample obj = new ThreadLocalExample();
- for(int i=0 ; i<10; i++){
- Thread t = new Thread(obj, ""+i);
- Thread.sleep(new Random().nextInt(1000));
- t.start();
- }
- }
-
- @Override
- public void run() {
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
- try {
- Thread.sleep(new Random().nextInt(1000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //formatter pattern is changed here by thread, but it won't reflect to other threads
- formatter.set(new SimpleDateFormat());
-
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
- }
-
- }
-
- ```
-
- Output:
-
- ```
- Thread Name= 0 default Formatter = yyyyMMdd HHmm
- Thread Name= 0 formatter = yy-M-d ah:mm
- Thread Name= 1 default Formatter = yyyyMMdd HHmm
- Thread Name= 2 default Formatter = yyyyMMdd HHmm
- Thread Name= 1 formatter = yy-M-d ah:mm
- Thread Name= 3 default Formatter = yyyyMMdd HHmm
- Thread Name= 2 formatter = yy-M-d ah:mm
- Thread Name= 4 default Formatter = yyyyMMdd HHmm
- Thread Name= 3 formatter = yy-M-d ah:mm
- Thread Name= 4 formatter = yy-M-d ah:mm
- Thread Name= 5 default Formatter = yyyyMMdd HHmm
- Thread Name= 5 formatter = yy-M-d ah:mm
- Thread Name= 6 default Formatter = yyyyMMdd HHmm
- Thread Name= 6 formatter = yy-M-d ah:mm
- Thread Name= 7 default Formatter = yyyyMMdd HHmm
- Thread Name= 7 formatter = yy-M-d ah:mm
- Thread Name= 8 default Formatter = yyyyMMdd HHmm
- Thread Name= 9 default Formatter = yyyyMMdd HHmm
- Thread Name= 8 formatter = yy-M-d ah:mm
- Thread Name= 9 formatter = yy-M-d ah:mm
- ```
-
- 从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。
-
- 上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。
-
- ```java
- private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
- @Override
- protected SimpleDateFormat initialValue()
- {
- return new SimpleDateFormat("yyyyMMdd HHmm");
- }
- };
- ```
-
- ### 3.3. ThreadLocal原理
-
- 从 `Thread`类源代码入手。
-
- ```java
- public class Thread implements Runnable {
- ......
- //与此线程有关的ThreadLocal值。由ThreadLocal类维护
- ThreadLocal.ThreadLocalMap threadLocals = null;
-
- //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
- ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- ......
- }
- ```
-
- 从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。
-
- `ThreadLocal`类的`set()`方法
-
- ```java
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- ```
-
- 通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。
-
- **每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了 ThreadLocal 声明的变量为什么在每一个线程都有自己的专属本地变量。
-
- `ThreadLocalMap`是`ThreadLocal`的静态内部类。
-
- ![ThreadLocal内部类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadLocal内部类.png)
-
- ### 3.4. ThreadLocal 内存泄露问题
-
- `ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
-
- ```java
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
-
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- ```
-
- **弱引用介绍:**
-
- > 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
- >
- > 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
-
- ## 4. 线程池
-
- ### 4.1. 为什么要用线程池?
-
- > **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。**
-
- **线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。
-
- 这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**:
-
- - **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- - **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- - **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
- ### 4.2. 实现Runnable接口和Callable接口的区别
-
- `Runnable`自Java 1.0以来一直存在,但`Callable`仅在Java 1.5中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。
-
- 工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。
-
- `Runnable.java`
-
- ```java
- @FunctionalInterface
- public interface Runnable {
- /**
- * 被线程执行,没有返回值也无法抛出异常
- */
- public abstract void run();
- }
- ```
-
- `Callable.java`
-
- ```java
- @FunctionalInterface
- public interface Callable<V> {
- /**
- * 计算结果,或在无法这样做时抛出异常。
- * @return 计算得出的结果
- * @throws 如果无法计算结果,则抛出异常
- */
- V call() throws Exception;
- }
- ```
-
- ### 4.3. 执行execute()方法和submit()方法的区别是什么呢?
-
- 1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
- 2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
-
- 我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码:
-
- ```java
- public Future<?> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<Void> ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
- ```
-
- 上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。
-
- ```java
- protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
- return new FutureTask<T>(runnable, value);
- }
- ```
-
- 我们再来看看`execute()`方法:
-
- ```java
- public void execute(Runnable command) {
- ...
- }
- ```
-
- ### 4.4. 如何创建线程池
-
- 《阿里巴巴Java开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
-
- > Executors 返回线程池对象的弊端如下:
- >
- > - **FixedThreadPool 和 SingleThreadExecutor** : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
- > - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。
-
- **方式一:通过构造方法实现**
- ![ThreadPoolExecutor构造方法](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/ThreadPoolExecutor构造方法.png)
- **方式二:通过Executor 框架的工具类Executors来实现**
- 我们可以创建三种类型的ThreadPoolExecutor:
-
- - **FixedThreadPool** : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
- - **SingleThreadExecutor:** 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
- - **CachedThreadPool:** 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
-
- 对应Executors工具类中的方法如图所示:
- ![Executor框架的工具类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/Executor框架的工具类.png)
-
- ### 4.5 ThreadPoolExecutor 类分析
-
- `ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。
-
- ```java
- /**
- * 用给定的初始参数创建一个新的ThreadPoolExecutor。
- */
- public ThreadPoolExecutor(int corePoolSize,
- int maximumPoolSize,
- long keepAliveTime,
- TimeUnit unit,
- BlockingQueue<Runnable> workQueue,
- ThreadFactory threadFactory,
- RejectedExecutionHandler handler) {
- if (corePoolSize < 0 ||
- maximumPoolSize <= 0 ||
- maximumPoolSize < corePoolSize ||
- keepAliveTime < 0)
- throw new IllegalArgumentException();
- if (workQueue == null || threadFactory == null || handler == null)
- throw new NullPointerException();
- this.corePoolSize = corePoolSize;
- this.maximumPoolSize = maximumPoolSize;
- this.workQueue = workQueue;
- this.keepAliveTime = unit.toNanos(keepAliveTime);
- this.threadFactory = threadFactory;
- this.handler = handler;
- }
- ```
-
- **下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。**
-
- #### 4.5.1 `ThreadPoolExecutor`构造函数重要参数分析
-
- **`ThreadPoolExecutor` 3 个最重要的参数:**
-
- - **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
- - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- - **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
-
- `ThreadPoolExecutor`其他常见参数:
-
- 1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁;
- 2. **`unit`** : `keepAliveTime` 参数的时间单位。
- 3. **`threadFactory`** :executor 创建新线程的时候会用到。
- 4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。
-
- #### 4.5.2 `ThreadPoolExecutor` 饱和策略
-
- **`ThreadPoolExecutor` 饱和策略定义:**
-
- 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略:
-
- - **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。
- - **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- - **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
- - **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
-
- 举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
-
- ### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor`
-
- 为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
-
- 首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
-
- `MyRunnable.java`
-
- ```java
- import java.util.Date;
-
- /**
- * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
- * @author shuang.kou
- */
- public class MyRunnable implements Runnable {
-
- private String command;
-
- public MyRunnable(String s) {
- this.command = s;
- }
-
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
- processCommand();
- System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
- }
-
- private void processCommand() {
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public String toString() {
- return this.command;
- }
- }
-
- ```
-
- 编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
-
- `ThreadPoolExecutorDemo.java`
-
- ```java
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
-
- public class ThreadPoolExecutorDemo {
-
- private static final int CORE_POOL_SIZE = 5;
- private static final int MAX_POOL_SIZE = 10;
- private static final int QUEUE_CAPACITY = 100;
- private static final Long KEEP_ALIVE_TIME = 1L;
- public static void main(String[] args) {
-
- //使用阿里巴巴推荐的创建线程池的方式
- //通过ThreadPoolExecutor构造函数自定义参数创建
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- CORE_POOL_SIZE,
- MAX_POOL_SIZE,
- KEEP_ALIVE_TIME,
- TimeUnit.SECONDS,
- new ArrayBlockingQueue<>(QUEUE_CAPACITY),
- new ThreadPoolExecutor.CallerRunsPolicy());
-
- for (int i = 0; i < 10; i++) {
- //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
- Runnable worker = new MyRunnable("" + i);
- //执行Runnable
- executor.execute(worker);
- }
- //终止线程池
- executor.shutdown();
- while (!executor.isTerminated()) {
- }
- System.out.println("Finished all threads");
- }
- }
-
- ```
-
- 可以看到我们上面的代码指定了:
-
- 1. `corePoolSize`: 核心线程数为 5。
- 2. `maximumPoolSize` :最大线程数 10
- 3. `keepAliveTime` : 等待时间为 1L。
- 4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
- 5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
- 6. `handler`:饱和策略为 `CallerRunsPolicy`。
-
- **Output:**
-
- ```
- pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
-
- ```
-
- ### 4.7 线程池原理分析
-
- 承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
-
- 现在,我们就分析上面的输出内容来简单分析一下线程池原理。
-
- **为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
-
- ```java
- // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
- private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
-
- private static int workerCountOf(int c) {
- return c & CAPACITY;
- }
-
- private final BlockingQueue<Runnable> workQueue;
-
- public void execute(Runnable command) {
- // 如果任务为null,则抛出异常。
- if (command == null)
- throw new NullPointerException();
- // ctl 中保存的线程池当前的一些状态信息
- int c = ctl.get();
-
- // 下面会涉及到 3 步 操作
- // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
- // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
- // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
- // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
- if (isRunning(c) && workQueue.offer(command)) {
- int recheck = ctl.get();
- // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
- if (!isRunning(recheck) && remove(command))
- reject(command);
- // 如果当前线程池为空就新创建一个线程并执行。
- else if (workerCountOf(recheck) == 0)
- addWorker(null, false);
- }
- //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
- else if (!addWorker(command, false))
- reject(command);
- }
- ```
-
- 通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
-
- ![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png)
-
- 现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
-
- 没搞懂的话,也没关系,可以看看我的分析:
-
- > 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
-
- ## 5. Atomic 原子类
-
- ### 5.1. 介绍一下Atomic 原子类
-
- Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
-
- 所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
-
-
- 并发包 `java.util.concurrent` 的原子类都存放在`java.util.concurrent.atomic`下,如下图所示。
-
- ![JUC原子类概览](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JUC原子类概览.png)
-
- ### 5.2. JUC 包中的原子类是哪4类?
-
- **基本类型**
-
- 使用原子的方式更新基本类型
-
- - AtomicInteger:整形原子类
- - AtomicLong:长整型原子类
- - AtomicBoolean:布尔型原子类
-
- **数组类型**
-
- 使用原子的方式更新数组里的某个元素
-
-
- - AtomicIntegerArray:整形数组原子类
- - AtomicLongArray:长整形数组原子类
- - AtomicReferenceArray:引用类型数组原子类
-
- **引用类型**
-
- - AtomicReference:引用类型原子类
- - AtomicStampedReference:原子更新引用类型里的字段原子类
- - AtomicMarkableReference :原子更新带有标记位的引用类型
-
- **对象的属性修改类型**
-
- - AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- - AtomicLongFieldUpdater:原子更新长整形字段的更新器
- - AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
-
-
- ### 5.3. 讲讲 AtomicInteger 的使用
-
- **AtomicInteger 类常用方法**
-
- ```java
- public final int get() //获取当前的值
- public final int getAndSet(int newValue)//获取当前的值,并设置新的值
- public final int getAndIncrement()//获取当前的值,并自增
- public final int getAndDecrement() //获取当前的值,并自减
- public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
- boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
- public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
- ```
-
- **AtomicInteger 类的使用示例**
-
- 使用 AtomicInteger 之后,不用对 increment() 方法加锁也可以保证线程安全。
- ```java
- class AtomicIntegerTest {
- private AtomicInteger count = new AtomicInteger();
- //使用AtomicInteger之后,不需要对该方法加锁,也可以实现线程安全。
- public void increment() {
- count.incrementAndGet();
- }
-
- public int getCount() {
- return count.get();
- }
- }
-
- ```
-
- ### 5.4. 能不能给我简单介绍一下 AtomicInteger 类的原理
-
- AtomicInteger 线程安全原理简单分析
-
- AtomicInteger 类的部分源码:
-
- ```java
- // setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供“比较并替换”的作用)
- private static final Unsafe unsafe = Unsafe.getUnsafe();
- private static final long valueOffset;
-
- static {
- try {
- valueOffset = unsafe.objectFieldOffset
- (AtomicInteger.class.getDeclaredField("value"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- private volatile int value;
- ```
-
- AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
-
- CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
-
- 关于 Atomic 原子类这部分更多内容可以查看我的这篇文章:并发编程面试必备:[JUC 中的 Atomic 原子类总结](https://mp.weixin.qq.com/s/joa-yOiTrYF67bElj8xqvg)
-
- ## 6. AQS
-
- ### 6.1. AQS 介绍
-
- AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包下面。
-
- ![AQS类](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS类.png)
-
- AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
-
- ### 6.2. AQS 原理分析
-
- AQS 原理这部分参考了部分博客,在5.2节末尾放了链接。
-
- > 在面试中被问到并发知识的时候,大多都会被问到“请你说一下自己对于AQS原理的理解”。下面给大家一个示例供大家参加,面试不是背题,大家一定要加入自己的思想,即使加入不了自己的思想也要保证自己能够通俗的讲出来而不是背出来。
-
- 下面大部分内容其实在AQS类注释上已经给出了,不过是英语看着比较吃力一点,感兴趣的话可以看看源码。
-
- #### 6.2.1. AQS 原理概览
-
- **AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。**
-
- > CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
-
- 看个AQS(AbstractQueuedSynchronizer)原理图:
-
-
- ![AQS原理图](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/AQS原理图.png)
-
- AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
-
- ```java
- private volatile int state;//共享变量,使用volatile修饰保证线程可见性
- ```
-
- 状态信息通过protected类型的getState,setState,compareAndSetState进行操作
-
- ```java
-
- //返回同步状态的当前值
- protected final int getState() {
- return state;
- }
- // 设置同步状态的值
- protected final void setState(int newState) {
- state = newState;
- }
- //原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
- protected final boolean compareAndSetState(int expect, int update) {
- return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
- }
- ```
-
- #### 6.2.2. AQS 对资源的共享方式
-
- **AQS定义两种资源共享方式**
-
- - **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
- - 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- - 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
- - **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
-
- ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
-
- 不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。
-
- #### 6.2.3. AQS底层使用了模板方法模式
-
- 同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
-
- 1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
- 2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
-
- 这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。
-
- **AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:**
-
- ```java
- isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
-
- ```
-
- 默认情况下,每个方法都抛出 `UnsupportedOperationException`。 这些方法的实现必须是内部线程安全的,并且通常应该简短而不是阻塞。AQS类中的其他方法都是final ,所以无法被其他类使用,只有这几个方法可以被其他类使用。
-
- 以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
-
- 再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
-
- 一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。
-
- 推荐两篇 AQS 原理和相关源码分析的文章:
-
- - http://www.cnblogs.com/waterystone/p/4920797.html
- - https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
-
- ### 6.3. AQS 组件总结
-
- - **Semaphore(信号量)-允许多个线程同时访问:** synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
- - **CountDownLatch (倒计时器):** CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
- - **CyclicBarrier(循环栅栏):** CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
-
- ## 7 Reference
-
- - 《深入理解 Java 虚拟机》
- - 《实战 Java 高并发程序设计》
- - 《Java并发编程的艺术》
- - http://www.cnblogs.com/waterystone/p/4920797.html
- - https://www.cnblogs.com/chengxiao/archive/2017/07/24/7141160.html
- - <https://www.journaldev.com/1076/java-threadlocal-example>
-
- ## 公众号
-
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-
- **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"面试突击"** 即可免费领取!
-
- **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
-
- ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- 点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
-
-
- - [Java 并发基础常见面试题总结](#java-并发基础常见面试题总结)
- - [1. 什么是线程和进程?](#1-什么是线程和进程)
- - [1.1. 何为进程?](#11-何为进程)
- - [1.2. 何为线程?](#12-何为线程)
- - [2. 请简要描述线程与进程的关系,区别及优缺点?](#2-请简要描述线程与进程的关系区别及优缺点)
- - [2.1. 图解进程和线程的关系](#21-图解进程和线程的关系)
- - [2.2. 程序计数器为什么是私有的?](#22-程序计数器为什么是私有的)
- - [2.3. 虚拟机栈和本地方法栈为什么是私有的?](#23-虚拟机栈和本地方法栈为什么是私有的)
- - [2.4. 一句话简单了解堆和方法区](#24-一句话简单了解堆和方法区)
- - [3. 说说并发与并行的区别?](#3-说说并发与并行的区别)
- - [4. 为什么要使用多线程呢?](#4-为什么要使用多线程呢)
- - [5. 使用多线程可能带来什么问题?](#5-使用多线程可能带来什么问题)
- - [6. 说说线程的生命周期和状态?](#6-说说线程的生命周期和状态)
- - [7. 什么是上下文切换?](#7-什么是上下文切换)
- - [8. 什么是线程死锁?如何避免死锁?](#8-什么是线程死锁如何避免死锁)
- - [8.1. 认识线程死锁](#81-认识线程死锁)
- - [8.2. 如何避免线程死锁?](#82-如何避免线程死锁)
- - [9. 说说 sleep() 方法和 wait() 方法区别和共同点?](#9-说说-sleep-方法和-wait-方法区别和共同点)
- - [10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?](#10-为什么我们调用-start-方法时会执行-run-方法为什么我们不能直接调用-run-方法)
- - [公众号](#公众号)
-
-
- # Java 并发基础常见面试题总结
-
- ## 1. 什么是线程和进程?
-
- ### 1.1. 何为进程?
-
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
-
- 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
-
- 如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
-
- ![进程示例图片-Windows](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/进程示例图片-Windows.png)
-
- ### 1.2. 何为线程?
-
- 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-
- Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
-
- ```java
- public class MultiThread {
- public static void main(String[] args) {
- // 获取 Java 线程管理 MXBean
- ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
- // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
- ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
- // 遍历线程信息,仅打印线程 ID 和线程名称信息
- for (ThreadInfo threadInfo : threadInfos) {
- System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
- }
- }
- }
- ```
-
- 上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
-
- ```
- [5] Attach Listener //添加事件
- [4] Signal Dispatcher // 分发处理给 JVM 信号的线程
- [3] Finalizer //调用对象 finalize 方法的线程
- [2] Reference Handler //清除 reference 线程
- [1] main //main 线程,程序入口
- ```
-
- 从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
-
- ## 2. 请简要描述线程与进程的关系,区别及优缺点?
-
- **从 JVM 角度说进程和线程之间的关系**
-
- ### 2.1. 图解进程和线程的关系
-
- 下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md "《可能是把 Java 内存区域讲的最清楚的一篇文章》")
-
- <div align="center">
- <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM运行时数据区域.png" width="600px"/>
- </div>
-
- 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
-
- **总结:** 线程 是 进程 划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
-
- 下面是该知识点的扩展内容!
-
- 下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
-
- ### 2.2. 程序计数器为什么是私有的?
-
- 程序计数器主要有下面两个作用:
-
- 1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
-
- 需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
-
- 所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
-
- ### 2.3. 虚拟机栈和本地方法栈为什么是私有的?
-
- - **虚拟机栈:** 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- - **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
-
- 所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
-
- ### 2.4. 一句话简单了解堆和方法区
-
- 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
- ## 3. 说说并发与并行的区别?
-
- - **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- - **并行:** 单位时间内,多个任务同时执行。
-
- ## 4. 为什么要使用多线程呢?
-
- 先从总体上来说:
-
- - **从计算机底层来说:** 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- - **从当代互联网发展趋势来说:** 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
-
- 再深入到计算机底层来探讨:
-
- - **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
- - **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
-
- ## 5. 使用多线程可能带来什么问题?
-
- 并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
-
- ## 6. 说说线程的生命周期和状态?
-
- Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
-
- ![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png)
-
- 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
-
- ![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java+%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png)
-
- 由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
-
- > 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/ "HowToDoInJava"):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/ "Java Thread Life Cycle and Thread States")),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
-
- ![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png)
-
- 当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)** 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的`run()`方法之后将会进入到 **TERMINATED(终止)** 状态。
-
- ## 7. 什么是上下文切换?
-
- 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
-
- 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
-
- 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
-
- Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-
- ## 8. 什么是线程死锁?如何避免死锁?
-
- ### 8.1. 认识线程死锁
-
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
-
- 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
-
- ![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4%E6%AD%BB%E9%94%811.png)
-
- 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
-
- ```java
- public class DeadLockDemo {
- private static Object resource1 = new Object();//资源 1
- private static Object resource2 = new Object();//资源 2
-
- public static void main(String[] args) {
- new Thread(() -> {
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource2");
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- }
- }
- }, "线程 1").start();
-
- new Thread(() -> {
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource1");
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- }
- }
- }, "线程 2").start();
- }
- }
- ```
-
- Output
-
- ```
- Thread[线程 1,5,main]get resource1
- Thread[线程 2,5,main]get resource2
- Thread[线程 1,5,main]waiting get resource2
- Thread[线程 2,5,main]waiting get resource1
- ```
-
- 线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过`Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
-
- 学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
-
- 1. 互斥条件:该资源任意一个时刻只由一个线程占用。
- 2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
- ### 8.2. 如何避免线程死锁?
-
- 我们只要破坏产生死锁的四个条件中的其中一个就可以了。
-
- **破坏互斥条件**
-
- 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
- **破坏请求与保持条件**
-
- 一次性申请所有的资源。
-
- **破坏不剥夺条件**
-
- 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
- **破坏循环等待条件**
-
- 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
-
- 我们对线程 2 的代码修改成下面这样就不会产生死锁了。
-
- ```java
- new Thread(() -> {
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource2");
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- }
- }
- }, "线程 2").start();
- ```
-
- Output
-
- ```
- Thread[线程 1,5,main]get resource1
- Thread[线程 1,5,main]waiting get resource2
- Thread[线程 1,5,main]get resource2
- Thread[线程 2,5,main]get resource1
- Thread[线程 2,5,main]waiting get resource2
- Thread[线程 2,5,main]get resource2
-
- Process finished with exit code 0
- ```
-
- 我们分析一下上面的代码为什么避免了死锁的发生?
-
- 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
-
- ## 9. 说说 sleep() 方法和 wait() 方法区别和共同点?
-
- - 两者最主要的区别在于:**sleep 方法没有释放锁,而 wait 方法释放了锁** 。
- - 两者都可以暂停线程的执行。
- - Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- - wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。
-
- ## 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
-
- 这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
-
- new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
-
- **总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。**
-
- ## 公众号
-
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-
- **《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取!
-
- **Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
-
- ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- <!-- TOC -->
-
- - [一 使用线程池的好处](#一-使用线程池的好处)
- - [二 Executor 框架](#二-executor-框架)
- - [2.1 简介](#21-简介)
- - [2.2 Executor 框架结构(主要由三大部分组成)](#22-executor-框架结构主要由三大部分组成)
- - [1) 任务(`Runnable` /`Callable`)](#1-任务runnable-callable)
- - [2) 任务的执行(`Executor`)](#2-任务的执行executor)
- - [3) 异步计算的结果(`Future`)](#3-异步计算的结果future)
- - [2.3 Executor 框架的使用示意图](#23-executor-框架的使用示意图)
- - [三 (重要)ThreadPoolExecutor 类简单介绍](#三-重要threadpoolexecutor-类简单介绍)
- - [3.1 ThreadPoolExecutor 类分析](#31-threadpoolexecutor-类分析)
- - [3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池](#32-推荐使用-threadpoolexecutor-构造函数创建线程池)
- - [四 (重要)ThreadPoolExecutor 使用示例](#四-重要threadpoolexecutor-使用示例)
- - [4.1 示例代码:`Runnable`+`ThreadPoolExecutor`](#41-示例代码runnablethreadpoolexecutor)
- - [4.2 线程池原理分析](#42-线程池原理分析)
- - [4.3 几个常见的对比](#43-几个常见的对比)
- - [4.3.1 `Runnable` vs `Callable`](#431-runnable-vs-callable)
- - [4.3.2 `execute()` vs `submit()`](#432-execute-vs-submit)
- - [4.3.3 `shutdown()`VS`shutdownNow()`](#433-shutdownvsshutdownnow)
- - [4.3.2 `isTerminated()` VS `isShutdown()`](#432-isterminated-vs-isshutdown)
- - [4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码](#44-加餐callablethreadpoolexecutor示例代码)
- - [五 几种常见的线程池详解](#五-几种常见的线程池详解)
- - [5.1 FixedThreadPool](#51-fixedthreadpool)
- - [5.1.1 介绍](#511-介绍)
- - [5.1.2 执行任务过程介绍](#512-执行任务过程介绍)
- - [5.1.3 为什么不推荐使用`FixedThreadPool`?](#513-为什么不推荐使用fixedthreadpool)
- - [5.2 SingleThreadExecutor 详解](#52-singlethreadexecutor-详解)
- - [5.2.1 介绍](#521-介绍)
- - [5.2.2 执行任务过程介绍](#522-执行任务过程介绍)
- - [5.2.3 为什么不推荐使用`FixedThreadPool`?](#523-为什么不推荐使用fixedthreadpool)
- - [5.3 CachedThreadPool 详解](#53-cachedthreadpool-详解)
- - [5.3.1 介绍](#531-介绍)
- - [5.3.2 执行任务过程介绍](#532-执行任务过程介绍)
- - [5.3.3 为什么不推荐使用`CachedThreadPool`?](#533-为什么不推荐使用cachedthreadpool)
- - [六 ScheduledThreadPoolExecutor 详解](#六-scheduledthreadpoolexecutor-详解)
- - [6.1 简介](#61-简介)
- - [6.2 运行机制](#62-运行机制)
- - [6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤](#63-scheduledthreadpoolexecutor-执行周期任务的步骤)
- - [七 线程池大小确定](#七-线程池大小确定)
- - [八 参考](#八-参考)
- - [九 其他推荐阅读](#九-其他推荐阅读)
-
- <!-- /TOC -->
-
- ## 一 使用线程池的好处
-
- > **池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。**
-
- **线程池**提供了一种限制和管理资源(包括执行一个任务)。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。
-
- 这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**:
-
- - **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- - **提高响应速度**。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- - **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
-
- ## 二 Executor 框架
-
- ### 2.1 简介
-
- Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。
-
- > 补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。
-
- Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。
-
- ### 2.2 Executor 框架结构(主要由三大部分组成)
-
- #### 1) 任务(`Runnable` /`Callable`)
-
- 执行任务需要实现的 **`Runnable` 接口** 或 **`Callable`接口**。**`Runnable` 接口**或 **`Callable` 接口** 实现类都可以被 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。
-
- #### 2) 任务的执行(`Executor`)
-
- 如下图所示,包括任务执行机制的核心接口 **`Executor`** ,以及继承自 `Executor` 接口的 **`ExecutorService` 接口。`ThreadPoolExecutor`** 和 **`ScheduledThreadPoolExecutor`** 这两个关键类实现了 **ExecutorService 接口**。
-
- **这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 `ThreadPoolExecutor` 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。**
-
- > **注意:** 通过查看 `ScheduledThreadPoolExecutor` 源代码我们发现 `ScheduledThreadPoolExecutor` 实际上是继承了 `ThreadPoolExecutor` 并实现了 ScheduledExecutorService ,而 `ScheduledExecutorService` 又实现了 `ExecutorService`,正如我们下面给出的类关系图显示的一样。
-
- **`ThreadPoolExecutor` 类描述:**
-
- ```java
- //AbstractExecutorService实现了ExecutorService接口
- public class ThreadPoolExecutor extends AbstractExecutorService
- ```
-
- **`ScheduledThreadPoolExecutor` 类描述:**
-
- ```java
- //ScheduledExecutorService实现了ExecutorService接口
- public class ScheduledThreadPoolExecutor
- extends ThreadPoolExecutor
- implements ScheduledExecutorService
- ```
-
- ![任务的执行相关接口](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/任务的执行相关接口.png)
-
- #### 3) 异步计算的结果(`Future`)
-
- **`Future`** 接口以及 `Future` 接口的实现类 **`FutureTask`** 类都可以代表异步计算的结果。
-
- 当我们把 **`Runnable`接口** 或 **`Callable` 接口** 的实现类提交给 **`ThreadPoolExecutor`** 或 **`ScheduledThreadPoolExecutor`** 执行。(调用 `submit()` 方法时会返回一个 **`FutureTask`** 对象)
-
- ### 2.3 Executor 框架的使用示意图
-
- ![Executor 框架的使用示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzg0ODIzMzMwLmpwZw?x-oss-process=image/format,png)
-
- 1. **主线程首先要创建实现 `Runnable` 或者 `Callable` 接口的任务对象。**
- 2. **把创建完成的实现 `Runnable`/`Callable`接口的 对象直接交给 `ExecutorService` 执行**: `ExecutorService.execute(Runnable command)`)或者也可以把 `Runnable` 对象或`Callable` 对象提交给 `ExecutorService` 执行(`ExecutorService.submit(Runnable task)`或 `ExecutorService.submit(Callable <T> task)`)。
- 3. **如果执行 `ExecutorService.submit(…)`,`ExecutorService` 将返回一个实现`Future`接口的对象**(我们刚刚也提到过了执行 `execute()`方法和 `submit()`方法的区别,`submit()`会返回一个 `FutureTask 对象)。由于 FutureTask` 实现了 `Runnable`,我们也可以创建 `FutureTask`,然后直接交给 `ExecutorService` 执行。
- 4. **最后,主线程可以执行 `FutureTask.get()`方法来等待任务执行完成。主线程也可以执行 `FutureTask.cancel(boolean mayInterruptIfRunning)`来取消此任务的执行。**
-
- ## 三 (重要)ThreadPoolExecutor 类简单介绍
-
- **线程池实现类 `ThreadPoolExecutor` 是 `Executor` 框架最核心的类。**
-
- ### 3.1 ThreadPoolExecutor 类分析
-
- `ThreadPoolExecutor` 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。
-
- ```java
- /**
- * 用给定的初始参数创建一个新的ThreadPoolExecutor。
- */
- public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量
- int maximumPoolSize,//线程池的最大线程数
- long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间
- TimeUnit unit,//时间单位
- BlockingQueue<Runnable> workQueue,//任务队列,用来储存等待执行任务的队列
- ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可
- RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
- ) {
- if (corePoolSize < 0 ||
- maximumPoolSize <= 0 ||
- maximumPoolSize < corePoolSize ||
- keepAliveTime < 0)
- throw new IllegalArgumentException();
- if (workQueue == null || threadFactory == null || handler == null)
- throw new NullPointerException();
- this.corePoolSize = corePoolSize;
- this.maximumPoolSize = maximumPoolSize;
- this.workQueue = workQueue;
- this.keepAliveTime = unit.toNanos(keepAliveTime);
- this.threadFactory = threadFactory;
- this.handler = handler;
- }
- ```
-
- **下面这些对创建 非常重要,在后面使用线程池的过程中你一定会用到!所以,务必拿着小本本记清楚。**
-
- **`ThreadPoolExecutor` 3 个最重要的参数:**
-
- - **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。
- - **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
- - **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
-
- `ThreadPoolExecutor`其他常见参数:
-
- 1. **`keepAliveTime`**:当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁;
- 2. **`unit`** : `keepAliveTime` 参数的时间单位。
- 3. **`threadFactory`** :executor 创建新线程的时候会用到。
- 4. **`handler`** :饱和策略。关于饱和策略下面单独介绍一下。
-
- 下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java性能调优实战》):
-
- ![线程池各个参数的关系](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/线程池各个参数的关系.jpg)
-
- **`ThreadPoolExecutor` 饱和策略定义:**
-
- 如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任时,`ThreadPoolTaskExecutor` 定义一些策略:
-
- - **`ThreadPoolExecutor.AbortPolicy`**:抛出 `RejectedExecutionException`来拒绝新任务的处理。
- - **`ThreadPoolExecutor.CallerRunsPolicy`**:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行(`run`)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- - **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
- - **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
-
- 举个例子:
-
- > Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)
-
- ### 3.2 推荐使用 `ThreadPoolExecutor` 构造函数创建线程池
-
- **在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显示创建线程。**
-
- **为什么呢?**
-
- > **使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。**
-
- **另外《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险**
-
- > Executors 返回线程池对象的弊端如下:
- >
- > - **`FixedThreadPool` 和 `SingleThreadExecutor`** : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
- > - **CachedThreadPool 和 ScheduledThreadPool** : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
-
- **方式一:通过`ThreadPoolExecutor`构造函数实现(推荐)**
- ![通过构造方法实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE3ODU4MjMwLmpwZw?x-oss-process=image/format,png)
- **方式二:通过 Executor 框架的工具类 Executors 来实现**
- 我们可以创建三种类型的 ThreadPoolExecutor:
-
- - **FixedThreadPool**
- - **SingleThreadExecutor**
- - **CachedThreadPool**
-
- 对应 Executors 工具类中的方法如图所示:
- ![通过Executor 框架的工具类Executors来实现](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzEzMjk2OTAxLmpwZw?x-oss-process=image/format,png)
-
- ## 四 (重要)ThreadPoolExecutor 使用示例
-
- 我们上面讲解了 `Executor`框架以及 `ThreadPoolExecutor` 类,下面让我们实战一下,来通过写一个 `ThreadPoolExecutor` 的小 Demo 来回顾上面的内容。
-
- ### 4.1 示例代码:`Runnable`+`ThreadPoolExecutor`
-
- 首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
-
- `MyRunnable.java`
-
- ```java
- import java.util.Date;
-
- /**
- * 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
- * @author shuang.kou
- */
- public class MyRunnable implements Runnable {
-
- private String command;
-
- public MyRunnable(String s) {
- this.command = s;
- }
-
- @Override
- public void run() {
- System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
- processCommand();
- System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
- }
-
- private void processCommand() {
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public String toString() {
- return this.command;
- }
- }
-
- ```
-
- 编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
-
- `ThreadPoolExecutorDemo.java`
-
- ```java
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
-
- public class ThreadPoolExecutorDemo {
-
- private static final int CORE_POOL_SIZE = 5;
- private static final int MAX_POOL_SIZE = 10;
- private static final int QUEUE_CAPACITY = 100;
- private static final Long KEEP_ALIVE_TIME = 1L;
- public static void main(String[] args) {
-
- //使用阿里巴巴推荐的创建线程池的方式
- //通过ThreadPoolExecutor构造函数自定义参数创建
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- CORE_POOL_SIZE,
- MAX_POOL_SIZE,
- KEEP_ALIVE_TIME,
- TimeUnit.SECONDS,
- new ArrayBlockingQueue<>(QUEUE_CAPACITY),
- new ThreadPoolExecutor.CallerRunsPolicy());
-
- for (int i = 0; i < 10; i++) {
- //创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
- Runnable worker = new MyRunnable("" + i);
- //执行Runnable
- executor.execute(worker);
- }
- //终止线程池
- executor.shutdown();
- while (!executor.isTerminated()) {
- }
- System.out.println("Finished all threads");
- }
- }
-
- ```
-
- 可以看到我们上面的代码指定了:
-
- 1. `corePoolSize`: 核心线程数为 5。
- 2. `maximumPoolSize` :最大线程数 10
- 3. `keepAliveTime` : 等待时间为 1L。
- 4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
- 5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
- 6. `handler`:饱和策略为 `CallerRunsPolicy`。
-
- **Output:**
-
- ```
- pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
- pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
- pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
- pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
-
- ```
-
- ### 4.2 线程池原理分析
-
- 承接 4.1 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
-
- 现在,我们就分析上面的输出内容来简单分析一下线程池原理。
-
- **为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.1 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
-
- ```java
- // 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
- private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
-
- private static int workerCountOf(int c) {
- return c & CAPACITY;
- }
-
- private final BlockingQueue<Runnable> workQueue;
-
- public void execute(Runnable command) {
- // 如果任务为null,则抛出异常。
- if (command == null)
- throw new NullPointerException();
- // ctl 中保存的线程池当前的一些状态信息
- int c = ctl.get();
-
- // 下面会涉及到 3 步 操作
- // 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
- // 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- if (workerCountOf(c) < corePoolSize) {
- if (addWorker(command, true))
- return;
- c = ctl.get();
- }
- // 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
- // 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
- if (isRunning(c) && workQueue.offer(command)) {
- int recheck = ctl.get();
- // 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
- if (!isRunning(recheck) && remove(command))
- reject(command);
- // 如果当前线程池为空就新创建一个线程并执行。
- else if (workerCountOf(recheck) == 0)
- addWorker(null, false);
- }
- //3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
- //如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
- else if (!addWorker(command, false))
- reject(command);
- }
- ```
-
- 通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
-
- ![图解线程池实现原理](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-7/图解线程池实现原理.png)
-
- 现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
-
- 没搞懂的话,也没关系,可以看看我的分析:
-
- > 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
-
- ### 4.3 几个常见的对比
-
- #### 4.3.1 `Runnable` vs `Callable`
-
- `Runnable`自 Java 1.0 以来一直存在,但`Callable`仅在 Java 1.5 中引入,目的就是为了来处理`Runnable`不支持的用例。**`Runnable` 接口**不会返回结果或抛出检查异常,但是**`Callable` 接口**可以。所以,如果任务不需要返回结果或抛出异常推荐使用 **`Runnable` 接口**,这样代码看起来会更加简洁。
-
- 工具类 `Executors` 可以实现 `Runnable` 对象和 `Callable` 对象之间的相互转换。(`Executors.callable(Runnable task`)或 `Executors.callable(Runnable task,Object resule)`)。
-
- `Runnable.java`
-
- ```java
- @FunctionalInterface
- public interface Runnable {
- /**
- * 被线程执行,没有返回值也无法抛出异常
- */
- public abstract void run();
- }
- ```
-
- `Callable.java`
-
- ```java
- @FunctionalInterface
- public interface Callable<V> {
- /**
- * 计算结果,或在无法这样做时抛出异常。
- * @return 计算得出的结果
- * @throws 如果无法计算结果,则抛出异常
- */
- V call() throws Exception;
- }
-
- ```
-
- #### 4.3.2 `execute()` vs `submit()`
-
- 1. **`execute()`方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;**
- 2. **`submit()`方法用于提交需要返回值的任务。线程池会返回一个 `Future` 类型的对象,通过这个 `Future` 对象可以判断任务是否执行成功**,并且可以通过 `Future` 的 `get()`方法来获取返回值,`get()`方法会阻塞当前线程直到任务完成,而使用 `get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
-
- 我们以**`AbstractExecutorService`**接口中的一个 `submit` 方法为例子来看看源代码:
-
- ```java
- public Future<?> submit(Runnable task) {
- if (task == null) throw new NullPointerException();
- RunnableFuture<Void> ftask = newTaskFor(task, null);
- execute(ftask);
- return ftask;
- }
- ```
-
- 上面方法调用的 `newTaskFor` 方法返回了一个 `FutureTask` 对象。
-
- ```java
- protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
- return new FutureTask<T>(runnable, value);
- }
- ```
-
- 我们再来看看`execute()`方法:
-
- ```java
- public void execute(Runnable command) {
- ...
- }
- ```
-
- #### 4.3.3 `shutdown()`VS`shutdownNow()`
-
- - **`shutdown()`** :关闭线程池,线程池的状态变为 `SHUTDOWN`。线程池不再接受新任务了,但是队列里的任务得执行完毕。
- - **`shutdownNow()`** :关闭线程池,线程的状态变为 `STOP`。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。
-
- #### 4.3.2 `isTerminated()` VS `isShutdown()`
-
- - **`isShutDown`** 当调用 `shutdown()` 方法后返回为 true。
- - **`isTerminated`** 当调用 `shutdown()` 方法后,并且所有提交的任务完成后返回为 true
-
- ### 4.4 加餐:`Callable`+`ThreadPoolExecutor`示例代码
-
- `MyCallable.java`
-
- ```java
-
- import java.util.concurrent.Callable;
-
- public class MyCallable implements Callable<String> {
- @Override
- public String call() throws Exception {
- Thread.sleep(1000);
- //返回执行当前 Callable 的线程名字
- return Thread.currentThread().getName();
- }
- }
- ```
-
- `CallableDemo.java`
-
- ```java
-
- import java.util.ArrayList;
- import java.util.Date;
- import java.util.List;
- import java.util.concurrent.ArrayBlockingQueue;
- import java.util.concurrent.Callable;
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.Future;
- import java.util.concurrent.ThreadPoolExecutor;
- import java.util.concurrent.TimeUnit;
-
- public class CallableDemo {
-
- private static final int CORE_POOL_SIZE = 5;
- private static final int MAX_POOL_SIZE = 10;
- private static final int QUEUE_CAPACITY = 100;
- private static final Long KEEP_ALIVE_TIME = 1L;
-
- public static void main(String[] args) {
-
- //使用阿里巴巴推荐的创建线程池的方式
- //通过ThreadPoolExecutor构造函数自定义参数创建
- ThreadPoolExecutor executor = new ThreadPoolExecutor(
- CORE_POOL_SIZE,
- MAX_POOL_SIZE,
- KEEP_ALIVE_TIME,
- TimeUnit.SECONDS,
- new ArrayBlockingQueue<>(QUEUE_CAPACITY),
- new ThreadPoolExecutor.CallerRunsPolicy());
-
- List<Future<String>> futureList = new ArrayList<>();
- Callable<String> callable = new MyCallable();
- for (int i = 0; i < 10; i++) {
- //提交任务到线程池
- Future<String> future = executor.submit(callable);
- //将返回值 future 添加到 list,我们可以通过 future 获得 执行 Callable 得到的返回值
- futureList.add(future);
- }
- for (Future<String> fut : futureList) {
- try {
- System.out.println(new Date() + "::" + fut.get());
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
- }
- //关闭线程池
- executor.shutdown();
- }
- }
- ```
-
- Output:
-
- ```
- Wed Nov 13 13:40:41 CST 2019::pool-1-thread-1
- Wed Nov 13 13:40:42 CST 2019::pool-1-thread-2
- Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
- Wed Nov 13 13:40:42 CST 2019::pool-1-thread-4
- Wed Nov 13 13:40:42 CST 2019::pool-1-thread-5
- Wed Nov 13 13:40:42 CST 2019::pool-1-thread-3
- Wed Nov 13 13:40:43 CST 2019::pool-1-thread-2
- Wed Nov 13 13:40:43 CST 2019::pool-1-thread-1
- Wed Nov 13 13:40:43 CST 2019::pool-1-thread-4
- Wed Nov 13 13:40:43 CST 2019::pool-1-thread-5
- ```
-
- ## 五 几种常见的线程池详解
-
- ### 5.1 FixedThreadPool
-
- #### 5.1.1 介绍
-
- `FixedThreadPool` 被称为可重用固定线程数的线程池。通过 Executors 类中的相关源代码来看一下相关实现:
-
- ```java
- /**
- * 创建一个可重用固定数量线程的线程池
- */
- public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>(),
- threadFactory);
- }
- ```
-
- 另外还有一个 `FixedThreadPool` 的实现方法,和上面的类似,所以这里不多做阐述:
-
- ```java
- public static ExecutorService newFixedThreadPool(int nThreads) {
- return new ThreadPoolExecutor(nThreads, nThreads,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>());
- }
- ```
-
- **从上面源代码可以看出新创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。**
-
- #### 5.1.2 执行任务过程介绍
-
- `FixedThreadPool` 的 `execute()` 方法运行示意图(该图片来源:《Java 并发编程的艺术》):
-
- ![FixedThreadPool的execute()方法运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzcxMzc1OTYzLmpwZw?x-oss-process=image/format,png)
-
- **上图说明:**
-
- 1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
- 2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 `LinkedBlockingQueue`;
- 3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 `LinkedBlockingQueue` 中获取任务来执行;
-
- #### 5.1.3 为什么不推荐使用`FixedThreadPool`?
-
- **`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :**
-
- 1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
- 2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。
- 3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数;
- 4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
-
- ### 5.2 SingleThreadExecutor 详解
-
- #### 5.2.1 介绍
-
- `SingleThreadExecutor` 是只有一个线程的线程池。下面看看**SingleThreadExecutor 的实现:**
-
- ```java
- /**
- *返回只有一个线程的线程池
- */
- public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, 1,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>(),
- threadFactory));
- }
- ```
-
- ```java
- public static ExecutorService newSingleThreadExecutor() {
- return new FinalizableDelegatedExecutorService
- (new ThreadPoolExecutor(1, 1,
- 0L, TimeUnit.MILLISECONDS,
- new LinkedBlockingQueue<Runnable>()));
- }
- ```
-
- 从上面源代码可以看出新创建的 `SingleThreadExecutor` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 1.其他参数和 `FixedThreadPool` 相同。
-
- #### 5.2.2 执行任务过程介绍
-
- **`SingleThreadExecutor` 的运行示意图(该图片来源:《Java 并发编程的艺术》):**
- ![SingleThreadExecutor的运行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzgyMjc2NDU4LmpwZw?x-oss-process=image/format,png)
-
- **上图说明;**
-
- 1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
- 2. 当前线程池中有一个运行的线程后,将任务加入 `LinkedBlockingQueue`
- 3. 线程执行完当前的任务后,会在循环中反复从` LinkedBlockingQueue` 中获取任务来执行;
-
- #### 5.2.3 为什么不推荐使用`FixedThreadPool`?
-
- `SingleThreadExecutor` 使用无界队列 `LinkedBlockingQueue` 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。`SingleThreadExecuto`r 使用无界队列作为线程池的工作队列会对线程池带来的影响与 `FixedThreadPool` 相同。说简单点就是可能会导致 OOM,
-
- ### 5.3 CachedThreadPool 详解
-
- #### 5.3.1 介绍
-
- `CachedThreadPool` 是一个会根据需要创建新线程的线程池。下面通过源码来看看 `CachedThreadPool` 的实现:
-
- ```java
- /**
- * 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
- */
- public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>(),
- threadFactory);
- }
-
- ```
-
- ```java
- public static ExecutorService newCachedThreadPool() {
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
- 60L, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>());
- }
- ```
-
- `CachedThreadPool` 的` corePoolSize` 被设置为空(0),`maximumPoolSize `被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。
-
- #### 5.3.2 执行任务过程介绍
-
- **CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):**
- ![CachedThreadPool的execute()方法的执行示意图](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzE4NjExNzY3LmpwZw?x-oss-process=image/format,png)
-
- **上图说明:**
-
- 1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2;
- 2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成;
-
- #### 5.3.3 为什么不推荐使用`CachedThreadPool`?
-
- `CachedThreadPool`允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
-
- ## 六 ScheduledThreadPoolExecutor 详解
-
- **`ScheduledThreadPoolExecutor` 主要用来在给定的延迟后运行任务,或者定期执行任务。** 这个在实际项目中基本不会被用到,所以对这部分大家只需要简单了解一下它的思想。关于如何在Spring Boot 中 实现定时任务,可以查看这篇文章[《5分钟搞懂如何在Spring Boot中Schedule Tasks》](https://github.com/Snailclimb/springboot-guide/blob/master/docs/advanced/SpringBoot-ScheduleTasks.md)。
-
- ### 6.1 简介
-
- **`ScheduledThreadPoolExecutor` 使用的任务队列 `DelayQueue` 封装了一个 `PriorityQueue`,`PriorityQueue` 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(`ScheduledFutureTask` 的 `time` 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(`ScheduledFutureTask` 的 `squenceNumber` 变量小的先执行)。**
-
- **`ScheduledThreadPoolExecutor` 和 `Timer` 的比较:**
-
- - `Timer` 对系统时钟的变化敏感,`ScheduledThreadPoolExecutor`不是;
- - `Timer` 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 `ScheduledThreadPoolExecutor` 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程;
- - 在`TimerTask` 中抛出的运行时异常会杀死一个线程,从而导致 `Timer` 死机:-( ...即计划任务将不再运行。`ScheduledThreadExecutor` 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 `afterExecute` 方法`ThreadPoolExecutor`)。抛出异常的任务将被取消,但其他任务将继续运行。
-
- **综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。**
-
- > **备注:** Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。
-
- ### 6.2 运行机制
-
- ![ScheduledThreadPoolExecutor运行机制](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC00LTE2LzkyNTk0Njk4LmpwZw?x-oss-process=image/format,png)
-
- **`ScheduledThreadPoolExecutor` 的执行主要分为两大部分:**
-
- 1. 当调用 `ScheduledThreadPoolExecutor` 的 **`scheduleAtFixedRate()`** 方法或者**`scheduleWirhFixedDelay()`** 方法时,会向 `ScheduledThreadPoolExecutor` 的 **`DelayQueue`** 添加一个实现了 **`RunnableScheduledFuture`** 接口的 **`ScheduledFutureTask`** 。
- 2. 线程池中的线程从 `DelayQueue` 中获取 `ScheduledFutureTask`,然后执行任务。
-
- **`ScheduledThreadPoolExecutor` 为了实现周期性的执行任务,对 `ThreadPoolExecutor `做了如下修改:**
-
- - 使用 **`DelayQueue`** 作为任务队列;
- - 获取任务的方不同
- - 执行周期任务后,增加了额外的处理
-
- ### 6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤
-
- ![ScheduledThreadPoolExecutor执行周期任务的步骤](https://imgconvert.csdnimg.cn/aHR0cDovL215LWJsb2ctdG8tdXNlLm9zcy1jbi1iZWlqaW5nLmFsaXl1bmNzLmNvbS8xOC01LTMwLzU5OTE2Mzg5LmpwZw?x-oss-process=image/format,png)
-
- 1. 线程 1 从 `DelayQueue` 中获取已到期的 `ScheduledFutureTask(DelayQueue.take())`。到期任务是指 `ScheduledFutureTask `的 time 大于等于当前系统的时间;
- 2. 线程 1 执行这个 `ScheduledFutureTask`;
- 3. 线程 1 修改 `ScheduledFutureTask` 的 time 变量为下次将要被执行的时间;
- 4. 线程 1 把这个修改 time 之后的 `ScheduledFutureTask` 放回 `DelayQueue` 中(`DelayQueue.add()`)。
-
- ## 七 线程池大小确定
-
- **线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。我们并没有考虑过这样大小的配置是否会带来什么问题,我自己就是这大部分程序员中的一个代表。**
-
- 由于笔主对如何确定线程池大小也没有什么实际经验,所以,这部分内容参考了网上很多文章/书籍。
-
- **首先,可以肯定的一点是线程池大小设置过大或者过小都会有问题。合适的才是最好,貌似在 95 % 的场景下都是合适的。**
-
- 如果阅读过我的上一篇关于线程池的文章的话,你一定知道:
-
- **如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。**
-
- **但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。**
-
- > 上下文切换:
- >
- > 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
- >
- > 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
- >
- > Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-
- 有一个简单并且适用面比较广的公式:
-
- - **CPU 密集型任务(N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- - **I/O 密集型任务(2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
-
- ## 八 参考
-
- - 《Java 并发编程的艺术》
- - [Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example](https://www.journaldev.com/2340/java-scheduler-scheduledexecutorservice-scheduledthreadpoolexecutor-example "Java Scheduler ScheduledExecutorService ScheduledThreadPoolExecutor Example")
- - [java.util.concurrent.ScheduledThreadPoolExecutor Example](https://examples.javacodegeeks.com/core-java/util/concurrent/scheduledthreadpoolexecutor/java-util-concurrent-scheduledthreadpoolexecutor-example/ "java.util.concurrent.ScheduledThreadPoolExecutor Example")
- - [ThreadPoolExecutor – Java Thread Pool Example](https://www.journaldev.com/1069/threadpoolexecutor-java-thread-pool-example-executorservice "ThreadPoolExecutor – Java Thread Pool Example")
-
- ## 九 其他推荐阅读
-
- - [Java 并发(三)线程池原理](https://www.cnblogs.com/warehouse/p/10720781.html "Java并发(三)线程池原理")
- - [如何优雅的使用和理解线程池](https://github.com/crossoverJie/JCSprout/blob/master/MD/ThreadPoolExecutor.md "如何优雅的使用和理解线程池")
- ![Synchronized 关键字使用、底层原理、JDK1.6 之后的底层优化以及 和ReenTrantLock 的对比](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/Java%20%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%85%E5%A4%87%EF%BC%9A%E5%B9%B6%E5%8F%91%E7%9F%A5%E8%AF%86%E7%B3%BB%E7%BB%9F%E6%80%BB%E7%BB%93/%E4%BA%8C%20%20Synchronized%20%E5%85%B3%E9%94%AE%E5%AD%97%E4%BD%BF%E7%94%A8%E3%80%81%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%86%E3%80%81JDK1.6%20%E4%B9%8B%E5%90%8E%E7%9A%84%E5%BA%95%E5%B1%82%E4%BC%98%E5%8C%96%E4%BB%A5%E5%8F%8A%20%E5%92%8CReenTrantLock%20%E7%9A%84%E5%AF%B9%E6%AF%94.png)
-
- ### synchronized关键字最主要的三种使用方式的总结
-
- - **修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁**
- - **修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁** 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,**因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁**。
- - **修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。** 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!
-
- 下面我已一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。
-
- 面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”
-
-
-
- **双重校验锁实现对象单例(线程安全)**
-
- ```java
- public class Singleton {
-
- private volatile static Singleton uniqueInstance;
-
- private Singleton() {
- }
-
- public static Singleton getUniqueInstance() {
- //先判断对象是否已经实例过,没有实例化过才进入加锁代码
- if (uniqueInstance == null) {
- //类对象加锁
- synchronized (Singleton.class) {
- if (uniqueInstance == null) {
- uniqueInstance = new Singleton();
- }
- }
- }
- return uniqueInstance;
- }
- }
- ```
- 另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。
-
- uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:
-
- 1. 为 uniqueInstance 分配内存空间
- 2. 初始化 uniqueInstance
- 3. 将 uniqueInstance 指向分配的内存地址
-
- 但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
-
- 使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
-
-
- ###synchronized 关键字底层原理总结
-
-
-
- **synchronized 关键字底层原理属于 JVM 层面。**
-
- **① synchronized 同步语句块的情况**
-
- ```java
- public class SynchronizedDemo {
- public void method() {
- synchronized (this) {
- System.out.println("synchronized 代码块");
- }
- }
- }
-
- ```
-
- 通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行`javap -c -s -v -l SynchronizedDemo.class`。
-
- ![synchronized 关键字原理](https://images.gitbook.cn/abc37c80-d21d-11e8-aab3-09d30029e0d5)
-
- 从上面我们可以看出:
-
- **synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。** 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
-
- **② synchronized 修饰方法的的情况**
-
- ```java
- public class SynchronizedDemo2 {
- public synchronized void method() {
- System.out.println("synchronized 方法");
- }
- }
-
- ```
-
- ![synchronized 关键字原理](https://images.gitbook.cn/7d407bf0-d21e-11e8-b2d6-1188c7e0dd7e)
-
- synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
-
-
- 在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
-
-
- ### JDK1.6 之后的底层优化
-
- JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
-
- 锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
- **①偏向锁**
-
- **引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉**。
-
- 偏向锁的“偏”就是偏心的偏,它的意思是会偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!关于偏向锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
-
- 但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。
-
- **② 轻量级锁**
-
- 倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的)。**轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。** 关于轻量级锁的加锁和解锁的原理可以查看《深入理解Java虚拟机:JVM高级特性与最佳实践》第二版的13章第三节锁优化。
-
- **轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!**
-
- **③ 自旋锁和自适应自旋**
-
- 轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。
-
- 互斥同步对性能最大的影响就是阻塞的实现,因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。
-
- **一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。** 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。**为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋**。
-
- 百度百科对自旋锁的解释:
-
- > 何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
-
- 自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过`--XX:+UseSpinning`参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。**自旋次数的默认值是10次,用户可以修改`--XX:PreBlockSpin`来更改**。
-
- 另外,**在 JDK1.6 中引入了自适应的自旋锁。自适应的自旋锁带来的改进就是:自旋的时间不在固定了,而是和前一次同一个锁上的自旋时间以及锁的拥有者的状态来决定,虚拟机变得越来越“聪明”了**。
-
- **④ 锁消除**
-
- 锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。
-
- **⑤ 锁粗化**
-
- 原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小,——直在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。
-
- 大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。
-
- ### Synchronized 和 ReenTrantLock 的对比
-
-
- **① 两者都是可重入锁**
-
- 两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
-
- **② synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API**
-
- synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。
-
- **③ ReenTrantLock 比 synchronized 增加了一些高级功能**
-
- 相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:**①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)**
-
- - **ReenTrantLock提供了一种能够中断等待锁的线程的机制**,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
- - **ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。** ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的`ReentrantLock(boolean fair)`构造方法来制定是否是公平的。
- - synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),**线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”** ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
-
- 如果你想使用上述功能,那么选择ReenTrantLock是一个不错的选择。
-
- **④ 性能已不是选择标准**
-
- 在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具体表示为:synchronized 关键字吞吐量随线程数的增加,下降得非常严重。而ReenTrantLock 基本保持一个比较稳定的水平。我觉得这也侧面反映了, synchronized 关键字还有非常大的优化余地。后续的技术发展也证明了这一点,我们上面也讲了在 JDK1.6 之后 JVM 团队对 synchronized 关键字做了很多优化。**JDK1.6 之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作**。
- [ThreadLocal造成OOM内存溢出案例演示与原理分析](https://blog.csdn.net/xlgen157387/article/details/78298840)
-
- [深入理解 Java 之 ThreadLocal 工作原理](<https://allenwu.itscoder.com/threadlocal-source>)
-
- ## ThreadLocal
-
- ### ThreadLocal简介
-
- 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** JDK中提供的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**
-
- **如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。**
-
- 再举个简单的例子:
-
- 比如有两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么ThreadLocal就是用来这两个线程竞争的。
-
- ### ThreadLocal示例
-
- 相信看了上面的解释,大家已经搞懂 ThreadLocal 类是个什么东西了。
-
- ```java
- import java.text.SimpleDateFormat;
- import java.util.Random;
-
- public class ThreadLocalExample implements Runnable{
-
- // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
- private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
-
- public static void main(String[] args) throws InterruptedException {
- ThreadLocalExample obj = new ThreadLocalExample();
- for(int i=0 ; i<10; i++){
- Thread t = new Thread(obj, ""+i);
- Thread.sleep(new Random().nextInt(1000));
- t.start();
- }
- }
-
- @Override
- public void run() {
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
- try {
- Thread.sleep(new Random().nextInt(1000));
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- //formatter pattern is changed here by thread, but it won't reflect to other threads
- formatter.set(new SimpleDateFormat());
-
- System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
- }
-
- }
-
- ```
-
- Output:
-
- ```
- Thread Name= 0 default Formatter = yyyyMMdd HHmm
- Thread Name= 0 formatter = yy-M-d ah:mm
- Thread Name= 1 default Formatter = yyyyMMdd HHmm
- Thread Name= 2 default Formatter = yyyyMMdd HHmm
- Thread Name= 1 formatter = yy-M-d ah:mm
- Thread Name= 3 default Formatter = yyyyMMdd HHmm
- Thread Name= 2 formatter = yy-M-d ah:mm
- Thread Name= 4 default Formatter = yyyyMMdd HHmm
- Thread Name= 3 formatter = yy-M-d ah:mm
- Thread Name= 4 formatter = yy-M-d ah:mm
- Thread Name= 5 default Formatter = yyyyMMdd HHmm
- Thread Name= 5 formatter = yy-M-d ah:mm
- Thread Name= 6 default Formatter = yyyyMMdd HHmm
- Thread Name= 6 formatter = yy-M-d ah:mm
- Thread Name= 7 default Formatter = yyyyMMdd HHmm
- Thread Name= 7 formatter = yy-M-d ah:mm
- Thread Name= 8 default Formatter = yyyyMMdd HHmm
- Thread Name= 9 default Formatter = yyyyMMdd HHmm
- Thread Name= 8 formatter = yy-M-d ah:mm
- Thread Name= 9 formatter = yy-M-d ah:mm
- ```
-
- 从输出中可以看出,Thread-0已经改变了formatter的值,但仍然是thread-2默认格式化程序与初始化值相同,其他线程也一样。
-
- 上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA会提示你转换为Java8的格式(IDEA真的不错!)。因为ThreadLocal类在Java 8中扩展,使用一个新的方法`withInitial()`,将Supplier功能接口作为参数。
-
- ```java
- private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
- @Override
- protected SimpleDateFormat initialValue()
- {
- return new SimpleDateFormat("yyyyMMdd HHmm");
- }
- };
- ```
-
- ### ThreadLocal原理
-
- 从 `Thread`类源代码入手。
-
- ```java
- public class Thread implements Runnable {
- ......
- //与此线程有关的ThreadLocal值。由ThreadLocal类维护
- ThreadLocal.ThreadLocalMap threadLocals = null;
-
- //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
- ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- ......
- }
- ```
-
- 从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set() `方法。
-
- `ThreadLocal`类的`set()`方法
-
- ```java
- public void set(T value) {
- Thread t = Thread.currentThread();
- ThreadLocalMap map = getMap(t);
- if (map != null)
- map.set(this, value);
- else
- createMap(t, value);
- }
- ThreadLocalMap getMap(Thread t) {
- return t.threadLocals;
- }
- ```
-
- 通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。**
-
- **每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为key的键值对。** 比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话,会使用 `Thread`内部都是使用仅有那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。`ThreadLocal` 是 map结构是为了让每个线程可以关联多个 `ThreadLocal`变量。这也就解释了ThreadLocal声明的变量为什么在每一个线程都有自己的专属本地变量。
-
- ```java
- public class Thread implements Runnable {
- ......
- //与此线程有关的ThreadLocal值。由ThreadLocal类维护
- ThreadLocal.ThreadLocalMap threadLocals = null;
-
- //与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
- ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
- ......
- }
- ```
-
- `ThreadLocalMap`是`ThreadLocal`的静态内部类。
-
- ![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg)
-
- ### ThreadLocal 内存泄露问题
-
- `ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候会 key 会被清理掉,而 value 不会被清理掉。这样一来,`ThreadLocalMap` 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后 最好手动调用`remove()`方法
-
- ```java
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
-
- Entry(ThreadLocal<?> k, Object v) {
- super(k);
- value = v;
- }
- }
- ```
-
- **弱引用介绍:**
-
- > 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
- >
- > 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
- 点击关注[公众号](#公众号 "公众号")及时获取笔主最新更新文章,并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
-
- <!-- MarkdownTOC -->
-
- - [一 JDK 提供的并发容器总结](#一-jdk-提供的并发容器总结 "一 JDK 提供的并发容器总结")
- - [二 ConcurrentHashMap](#二-concurrenthashmap "二 ConcurrentHashMap")
- - [三 CopyOnWriteArrayList](#三-copyonwritearraylist "三 CopyOnWriteArrayList")
- - [3.1 CopyOnWriteArrayList 简介](#31-copyonwritearraylist-简介 "3.1 CopyOnWriteArrayList 简介")
- - [3.2 CopyOnWriteArrayList 是如何做到的?](#32-copyonwritearraylist-是如何做到的? "3.2 CopyOnWriteArrayList 是如何做到的?")
- - [3.3 CopyOnWriteArrayList 读取和写入源码简单分析](#33-copyonwritearraylist-读取和写入源码简单分析 "3.3 CopyOnWriteArrayList 读取和写入源码简单分析")
- - [3.3.1 CopyOnWriteArrayList 读取操作的实现](#331-copyonwritearraylist-读取操作的实现 "3.3.1 CopyOnWriteArrayList 读取操作的实现")
- - [3.3.2 CopyOnWriteArrayList 写入操作的实现](#332-copyonwritearraylist-写入操作的实现 "3.3.2 CopyOnWriteArrayList 写入操作的实现")
- - [四 ConcurrentLinkedQueue](#四-concurrentlinkedqueue "四 ConcurrentLinkedQueue")
- - [五 BlockingQueue](#五-blockingqueue "五 BlockingQueue")
- - [5.1 BlockingQueue 简单介绍](#51-blockingqueue-简单介绍 "5.1 BlockingQueue 简单介绍")
- - [5.2 ArrayBlockingQueue](#52-arrayblockingqueue "5.2 ArrayBlockingQueue")
- - [5.3 LinkedBlockingQueue](#53-linkedblockingqueue "5.3 LinkedBlockingQueue")
- - [5.4 PriorityBlockingQueue](#54-priorityblockingqueue "5.4 PriorityBlockingQueue")
- - [六 ConcurrentSkipListMap](#六-concurrentskiplistmap "六 ConcurrentSkipListMap")
- - [七 参考](#七-参考 "七 参考")
-
- <!-- /MarkdownTOC -->
-
- ## 一 JDK 提供的并发容器总结
-
- JDK 提供的这些容器大部分在 `java.util.concurrent` 包中。
-
- - **ConcurrentHashMap:** 线程安全的 HashMap
- - **CopyOnWriteArrayList:** 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.
- - **ConcurrentLinkedQueue:** 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
- - **BlockingQueue:** 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
- - **ConcurrentSkipListMap:** 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
-
- ## 二 ConcurrentHashMap
-
- 我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 `Collections.synchronizedMap()` 方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
-
- 所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
-
- 关于 ConcurrentHashMap 相关问题,我在 [Java 集合框架常见面试题](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md "Java集合框架常见面试题") 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题:
-
- - [ConcurrentHashMap 和 Hashtable 的区别](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap-%E5%92%8C-hashtable-%E7%9A%84%E5%8C%BA%E5%88%AB "ConcurrentHashMap 和 Hashtable 的区别")
- - [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98.md#concurrenthashmap%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F%E5%BA%95%E5%B1%82%E5%85%B7%E4%BD%93%E5%AE%9E%E7%8E%B0 "ConcurrentHashMap线程安全的具体实现方式/底层具体实现")
-
- ## 三 CopyOnWriteArrayList
-
- ### 3.1 CopyOnWriteArrayList 简介
-
- ```java
- public class CopyOnWriteArrayList<E>
- extends Object
- implements List<E>, RandomAccess, Cloneable, Serializable
- ```
-
- 在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。
-
- 这和我们之前在多线程章节讲过 `ReentrantReadWriteLock` 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 `CopyOnWriteArrayList` 类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,`CopyOnWriteArrayList` 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。**那它是怎么做的呢?**
-
- ### 3.2 CopyOnWriteArrayList 是如何做到的?
-
- `CopyOnWriteArrayList` 类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
-
- 从 `CopyOnWriteArrayList` 的名字就能看出`CopyOnWriteArrayList` 是满足`CopyOnWrite` 的 ArrayList,所谓`CopyOnWrite` 也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。
-
- ### 3.3 CopyOnWriteArrayList 读取和写入源码简单分析
-
- #### 3.3.1 CopyOnWriteArrayList 读取操作的实现
-
- 读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。
-
- ```java
- /** The array, accessed only via getArray/setArray. */
- private transient volatile Object[] array;
- public E get(int index) {
- return get(getArray(), index);
- }
- @SuppressWarnings("unchecked")
- private E get(Object[] a, int index) {
- return (E) a[index];
- }
- final Object[] getArray() {
- return array;
- }
-
- ```
-
- #### 3.3.2 CopyOnWriteArrayList 写入操作的实现
-
- CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。
-
- ```java
- /**
- * Appends the specified element to the end of this list.
- *
- * @param e element to be appended to this list
- * @return {@code true} (as specified by {@link Collection#add})
- */
- public boolean add(E e) {
- final ReentrantLock lock = this.lock;
- lock.lock();//加锁
- try {
- Object[] elements = getArray();
- int len = elements.length;
- Object[] newElements = Arrays.copyOf(elements, len + 1);//拷贝新数组
- newElements[len] = e;
- setArray(newElements);
- return true;
- } finally {
- lock.unlock();//释放锁
- }
- }
- ```
-
- ## 四 ConcurrentLinkedQueue
-
- Java 提供的线程安全的 Queue 可以分为**阻塞队列**和**非阻塞队列**,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 **阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。**
-
- 从名字可以看出,`ConcurrentLinkedQueue`这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。
-
- ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。
-
- ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。
-
- ## 五 BlockingQueue
-
- ### 5.1 BlockingQueue 简单介绍
-
- 上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
-
- BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:
-
- ![BlockingQueue 的实现类](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/51622268.jpg)
-
- **下面主要介绍一下:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。**
-
- ### 5.2 ArrayBlockingQueue
-
- **ArrayBlockingQueue** 是 BlockingQueue 接口的有界队列实现类,底层采用**数组**来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。
-
- ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:
-
- ```java
- private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
- ```
-
- ### 5.3 LinkedBlockingQueue
-
- **LinkedBlockingQueue** 底层基于**单向链表**实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。
-
- **相关构造方法:**
-
- ```java
- /**
- *某种意义上的无界队列
- * Creates a {@code LinkedBlockingQueue} with a capacity of
- * {@link Integer#MAX_VALUE}.
- */
- public LinkedBlockingQueue() {
- this(Integer.MAX_VALUE);
- }
-
- /**
- *有界队列
- * Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
- *
- * @param capacity the capacity of this queue
- * @throws IllegalArgumentException if {@code capacity} is not greater
- * than zero
- */
- public LinkedBlockingQueue(int capacity) {
- if (capacity <= 0) throw new IllegalArgumentException();
- this.capacity = capacity;
- last = head = new Node<E>(null);
- }
- ```
-
- ### 5.4 PriorityBlockingQueue
-
- **PriorityBlockingQueue** 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 `compareTo()` 方法来指定元素排序规则,或者初始化时通过构造器参数 `Comparator` 来指定排序规则。
-
- PriorityBlockingQueue 并发控制采用的是 **ReentrantLock**,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,**如果空间不够的话会自动扩容**)。
-
- 简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。
-
- **推荐文章:**
-
- 《解读 Java 并发队列 BlockingQueue》
-
- [https://javadoop.com/post/java-concurrent-queue](https://javadoop.com/post/java-concurrent-queue "https://javadoop.com/post/java-concurrent-queue")
-
- ## 六 ConcurrentSkipListMap
-
- 下面这部分内容参考了极客时间专栏[《数据结构与算法之美》](https://time.geekbang.org/column/intro/126?code=zl3GYeAsRI4rEJIBNu5B/km7LSZsPDlGWQEpAYw5Vu0=&utm_term=SPoster "《数据结构与算法之美》")以及《实战 Java 高并发程序设计》。
-
- **为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。**
-
- 对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 **O(logn)** 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。
-
- 跳表的本质是同时维护了多个链表,并且链表是分层的,
-
- ![2级索引跳表](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/93666217.jpg)
-
- 最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。
-
- 跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。
-
- ![在跳表中查找元素18](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-12-9/32005738.jpg)
-
- 查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。
-
- 从上面很容易看出,**跳表是一种利用空间换时间的算法。**
-
- 使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。
-
- ## 七 参考
-
- - 《实战 Java 高并发程序设计》
- - https://javadoop.com/post/java-concurrent-queue
- - https://juejin.im/post/5aeebd02518825672f19c546
-
- ## 公众号
-
- 如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
-
- **《Java 面试突击》:** 由本文档衍生的专为面试而生的《Java 面试突击》V2.0 PDF 版本[公众号](#公众号 "公众号")后台回复 **"面试突击"** 即可免费领取!
-
- **Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
-
- ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
- # Java 并发基础知识
-
- Java 并发的基础知识,可能会在笔试中遇到,技术面试中也可能以并发知识环节提问的第一个问题出现。比如面试官可能会问你:“谈谈自己对于进程和线程的理解,两者的区别是什么?”
-
- **本节思维导图:**
-
- ## 一 进程和线程
-
- 进程和线程的对比这一知识点由于过于基础,所以在面试中很少碰到,但是极有可能会在笔试题中碰到。
-
- 常见的提问形式是这样的:**“什么是线程和进程?,请简要描述线程与进程的关系、区别及优缺点? ”**。
-
- ### 1.1. 何为进程?
-
- 进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。
-
- 在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。
-
- 如下图所示,在 windows 中通过查看任务管理器的方式,我们就可以清楚看到 window 当前运行的进程(.exe 文件的运行)。
-
- ![进程 ](https://images.gitbook.cn/a0929b60-d133-11e8-88a4-5328c5b70145)
-
- ### 1.2 何为线程?
-
- 线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的**堆**和**方法区**资源,但每个线程有自己的**程序计数器**、**虚拟机栈**和**本地方法栈**,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
-
- Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
-
- ```java
- public class MultiThread {
- public static void main(String[] args) {
- // 获取 Java 线程管理 MXBean
- ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
- // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息
- ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
- // 遍历线程信息,仅打印线程 ID 和线程名称信息
- for (ThreadInfo threadInfo : threadInfos) {
- System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
- }
- }
- }
- ```
-
- 上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
-
- ```
- [5] Attach Listener //添加事件
- [4] Signal Dispatcher // 分发处理给 JVM 信号的线程
- [3] Finalizer //调用对象 finalize 方法的线程
- [2] Reference Handler //清除 reference 线程
- [1] main //main 线程,程序入口
- ```
-
- 从上面的输出内容可以看出:**一个 Java 程序的运行是 main 线程和多个其他线程同时运行**。
-
- ### 1.3 从 JVM 角度说进程和线程之间的关系(重要)
-
- #### 1.3.1 图解进程和线程的关系
-
- 下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。如果你对 Java 内存区域 (运行时数据区) 这部分知识不太了解的话可以阅读一下我的这篇文章:[《可能是把 Java 内存区域讲的最清楚的一篇文章》](<https://github.com/Snailclimb/JavaGuide/blob/3965c02cc0f294b0bd3580df4868d5e396959e2e/Java%E7%9B%B8%E5%85%B3/%E5%8F%AF%E8%83%BD%E6%98%AF%E6%8A%8AJava%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F%E8%AE%B2%E7%9A%84%E6%9C%80%E6%B8%85%E6%A5%9A%E7%9A%84%E4%B8%80%E7%AF%87%E6%96%87%E7%AB%A0.md>)
-
- <div align="center">
- <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3Java%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9FJDK1.8.png" width="600px"/>
- </div>
-
-
- 从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的**堆**和**方法区 (JDK1.8 之后的元空间)**资源,但是每个线程有自己的**程序计数器**、**虚拟机栈** 和 **本地方法栈**。
-
- 下面来思考这样一个问题:为什么**程序计数器**、**虚拟机栈**和**本地方法栈**是线程私有的呢?为什么堆和方法区是线程共享的呢?
-
- #### 1.3.2 程序计数器为什么是私有的?
-
- 程序计数器主要有下面两个作用:
-
- 1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
-
- 需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
-
- 所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。
-
- #### 1.3.3 虚拟机栈和本地方法栈为什么是私有的?
-
- - **虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- - **本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: **虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
-
- 所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。
-
- #### 1.3.4 一句话简单了解堆和方法区
-
- 堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
-
- ## 二 多线程并发编程
-
- ### 2.1 并发与并行概念解读
-
- - **并发:** 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
- - **并行:**单位时间内,多个任务同时执行。
-
- ### 2.2 为什么要使用多线程?
-
- 先从总体上来说:
-
- - **从计算机底层来说:**线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
- - **从当代互联网发展趋势来说:**现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。
-
- 再深入到计算机底层来探讨:
-
- - **单核时代:** 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
- - **多核时代:** 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。
-
- ### 2.3 使用多线程可能带来的问题
-
- 并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。
-
- ## 三 线程的创建与运行
-
- 前两种实际上很少使用,一般都是用线程池的方式比较多一点。
-
- ### 3.1 继承 Thread 类的方式
-
-
- ```java
- public class MyThread extends Thread {
- @Override
- public void run() {
- super.run();
- System.out.println("MyThread");
- }
- }
- ```
- Run.java
-
- ```java
- public class Run {
-
- public static void main(String[] args) {
- MyThread mythread = new MyThread();
- mythread.start();
- System.out.println("运行结束");
- }
-
- }
-
- ```
- 运行结果:
- ![结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243e80f22a2d54?w=161&h=54&f=jpeg&s=7380)
-
- 从上面的运行结果可以看出:线程是一个子任务,CPU 以不确定的方式,或者说是以随机的时间来调用线程中的 run 方法。
-
- ### 3.2 实现 Runnable 接口的方式
-
- 推荐实现 Runnable 接口方式开发多线程,因为 Java 单继承但是可以实现多个接口。
-
- MyRunnable.java
-
- ```java
- public class MyRunnable implements Runnable {
- @Override
- public void run() {
- System.out.println("MyRunnable");
- }
- }
- ```
-
- Run.java
-
- ```java
- public class Run {
-
- public static void main(String[] args) {
- Runnable runnable=new MyRunnable();
- Thread thread=new Thread(runnable);
- thread.start();
- System.out.println("运行结束!");
- }
-
- }
- ```
- 运行结果:
- ![运行结果 ](https://user-gold-cdn.xitu.io/2018/3/20/16243f4373c6141a?w=137&h=46&f=jpeg&s=7316)
-
- ### 3.3 使用线程池的方式
-
- 使用线程池的方式也是最推荐的一种方式,另外,《阿里巴巴 Java 开发手册》在第一章第六节并发处理这一部分也强调到“线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”。这里就不给大家演示代码了,线程池这一节会详细介绍到这部分内容。
-
- ## 四 线程的生命周期和状态
-
- Java 线程在运行的生命周期中的指定时刻只可能处于下面 6 种不同状态的其中一个状态(图源《Java 并发编程艺术》4.1.4 节)。
-
- ![Java 线程的状态 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81.png)
-
- 线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示(图源《Java 并发编程艺术》4.1.4 节):
-
- ![Java 线程状态变迁 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/19-1-29/Java%20%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E5%8F%98%E8%BF%81.png)
-
-
-
- 由上图可以看出:线程创建之后它将处于 **NEW(新建)** 状态,调用 `start()` 方法后开始运行,线程这时候处于 **READY(可运行)** 状态。可运行状态的线程获得了 CPU 时间片(timeslice)后就处于 **RUNNING(运行)** 状态。
-
- > 操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:[HowToDoInJava](https://howtodoinjava.com/):[Java Thread Life Cycle and Thread States](https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/)),所以 Java 系统一般将这两个状态统称为 **RUNNABLE(运行中)** 状态 。
-
- ![RUNNABLE-VS-RUNNING](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/RUNNABLE-VS-RUNNING.png)
-
- 当线程执行 `wait()`方法之后,线程进入 **WAITING(等待)**状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 **TIME_WAITING(超时等待)** 状态相当于在等待状态的基础上增加了超时限制,比如通过 `sleep(long millis)`方法或 `wait(long millis)`方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 **BLOCKED(阻塞)** 状态。线程在执行 Runnable 的` run() `方法之后将会进入到 **TERMINATED(终止)** 状态。
-
- ## 五 线程优先级
-
- **理论上**来说系统会根据优先级来决定首先使哪个线程进入运行状态。当 CPU 比较闲的时候,设置线程优先级几乎不会有任何作用,而且很多操作系统压根不会不会理会你设置的线程优先级,所以不要让业务过度依赖于线程的优先级。
-
- 另外,**线程优先级具有继承特性**比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的。**线程优先级还具有随机性** 也就是说线程优先级高的不一定每一次都先执行完。
-
- Thread 类中包含的成员变量代表了线程的某些优先级。如**Thread.MIN_PRIORITY(常数 1)**,**Thread.NORM_PRIORITY(常数 5)**,**Thread.MAX_PRIORITY(常数 10)**。其中每个线程的优先级都在**1** 到**10** 之间,在默认情况下优先级都是**Thread.NORM_PRIORITY(常数 5)**。
-
- **一般情况下,不会对线程设定优先级别,更不会让某些业务严重地依赖线程的优先级别,比如权重,借助优先级设定某个任务的权重,这种方式是不可取的,一般定义线程的时候使用默认的优先级就好了。**
-
- **相关方法:**
-
- ```java
- public final void setPriority(int newPriority) //为线程设定优先级
- public final int getPriority() //获取线程的优先级
- ```
- **设置线程优先级方法源码:**
-
- ```java
- public final void setPriority(int newPriority) {
- ThreadGroup g;
- checkAccess();
- //线程游戏优先级不能小于 1 也不能大于 10,否则会抛出异常
- if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
- throw new IllegalArgumentException();
- }
- //如果指定的线程优先级大于该线程所在线程组的最大优先级,那么该线程的优先级将设为线程组的最大优先级
- if((g = getThreadGroup()) != null) {
- if (newPriority > g.getMaxPriority()) {
- newPriority = g.getMaxPriority();
- }
- setPriority0(priority = newPriority);
- }
- }
-
- ```
-
- ## 六 守护线程和用户线程
-
- **守护线程和用户线程简介:**
-
- - **用户 (User) 线程:**运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
- - **守护 (Daemon) 线程:**运行在后台,为其他前台线程服务.也可以说守护线程是 JVM 中非守护线程的 **“佣人”**。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作.
-
- main 函数所在的线程就是一个用户线程啊,main 函数启动的同时在 JVM 内部同时还启动了好多守护线程,比如垃圾回收线程。
-
- **那么守护线程和用户线程有什么区别呢?**
-
- 比较明显的区别之一是用户线程结束,JVM 退出,不管这个时候有没有守护线程运行。而守护线程不会影响 JVM 的退出。
-
- **注意事项:**
-
- 1. `setDaemon(true)`必须在`start()`方法前执行,否则会抛出 `IllegalThreadStateException` 异常
- 2. 在守护线程中产生的新线程也是守护线程
- 3. 不是所有的任务都可以分配给守护线程来执行,比如读写操作或者计算逻辑
- 4. 守护 (Daemon) 线程中不能依靠 finally 块的内容来确保执行关闭或清理资源的逻辑。因为我们上面也说过了一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作,所以守护 (Daemon) 线程中的 finally 语句块可能无法被执行。
-
- ## 七 上下文切换
-
- 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
-
- 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换会这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。
-
- 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
-
- Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
-
- ## 八 线程死锁
-
- ### 认识线程死锁
-
- 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
-
- 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
-
- ![线程死锁示意图 ](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-4/2019-4死锁1.png)
-
- 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
-
- ```java
- public class DeadLockDemo {
- private static Object resource1 = new Object();//资源 1
- private static Object resource2 = new Object();//资源 2
-
- public static void main(String[] args) {
- new Thread(() -> {
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource2");
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- }
- }
- }, "线程 1").start();
-
- new Thread(() -> {
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource1");
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- }
- }
- }, "线程 2").start();
- }
- }
- ```
-
- Output
-
- ```
- Thread[线程 1,5,main]get resource1
- Thread[线程 2,5,main]get resource2
- Thread[线程 1,5,main]waiting get resource2
- Thread[线程 2,5,main]waiting get resource1
- ```
-
- 线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过` Thread.sleep(1000);`让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
-
- 学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
-
- 1. 互斥条件:该资源任意一个时刻只由一个线程占用。
- 1. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 1. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 1. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
- ### 如何预防线程死锁?
-
- 我们只要破坏产生死锁的四个条件中的其中一个就可以了。
-
- **破坏互斥条件**
-
- 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
- **破坏请求与保持条件**
-
- 一次性申请所有的资源。
-
- **破坏不剥夺条件**
-
- 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
- **破坏循环等待条件**
-
- 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
-
- 我们对线程 2 的代码修改成下面这样就不会产生死锁了。
-
- ```java
- new Thread(() -> {
- synchronized (resource1) {
- System.out.println(Thread.currentThread() + "get resource1");
- try {
- Thread.sleep(1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread() + "waiting get resource2");
- synchronized (resource2) {
- System.out.println(Thread.currentThread() + "get resource2");
- }
- }
- }, "线程 2").start();
- ```
-
- Output
-
- ```
- Thread[线程 1,5,main]get resource1
- Thread[线程 1,5,main]waiting get resource2
- Thread[线程 1,5,main]get resource2
- Thread[线程 2,5,main]get resource1
- Thread[线程 2,5,main]waiting get resource2
- Thread[线程 2,5,main]get resource2
-
- Process finished with exit code 0
- ```
-
- 我们分析一下上面的代码为什么避免了死锁的发生?
-
- 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。
-
- ## 参考
-
- - 《Java 并发编程之美》
-
- - 《Java 并发编程的艺术》
-
- - https://howtodoinjava.com/java/multi-threading/java-thread-life-cycle-and-thread-states/
-
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。