无涯

无所谓无 无所谓有

一 使用线程池的好处

池化技术相比大家已经屡见不鲜了,线程池、数据库连接池、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 接口 实现类都可以被 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。

2) 任务的执行(Executor)

如下图所示,包括任务执行机制的核心接口 Executor ,以及继承自 Executor 接口的 ExecutorService 接口。ThreadPoolExecutorScheduledThreadPoolExecutor 这两个关键类实现了 ExecutorService 接口

这里提了很多底层的类关系,但是,实际上我们需要更多关注的是 ThreadPoolExecutor 这个类,这个类在我们实际使用线程池的过程中,使用频率还是非常高的。

注意: 通过查看 ScheduledThreadPoolExecutor 源代码我们发现 ScheduledThreadPoolExecutor 实际上是继承了 ThreadPoolExecutor 并实现了 ScheduledExecutorService ,而 ScheduledExecutorService 又实现了 ExecutorService,正如我们下面给出的类关系图显示的一样。

ThreadPoolExecutor 类描述:

1
2
//AbstractExecutorService实现了ExecutorService接口
public class ThreadPoolExecutor extends AbstractExecutorService

ScheduledThreadPoolExecutor 类描述:

1
2
3
4
//ScheduledExecutorService继承ExecutorService接口
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService

任务的执行相关接口

3) 异步计算的结果(Future)

Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。

当我们把 Runnable接口Callable 接口 的实现类提交给 ThreadPoolExecutorScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)

2.3 Executor 框架的使用示意图

Executor 框架的使用示意图

  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 类简单介绍

线程池实现类 ThreadPoolExecutorExecutor 框架最核心的类。

3.1 ThreadPoolExecutor 类分析

ThreadPoolExecutor 类中提供的四个构造方法。我们来看最长的那个,其余三个都是在这个构造方法的基础上产生(其他几个构造方法说白点都是给定某些默认参数的构造方法比如默认制定拒绝策略是什么),这里就不贴代码讲了,比较简单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 用给定的初始参数创建一个新的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 性能调优实战》):

线程池各个参数的关系

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 返回线程池对象的弊端如下:

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

方式一:通过ThreadPoolExecutor构造函数实现(推荐)
通过构造方法实现
方式二:通过 Executor 框架的工具类 Executors 来实现
我们可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

对应 Executors 工具类中的方法如图所示:
通过Executor 框架的工具类Executors来实现

四 (重要)ThreadPoolExecutor 使用示例

我们上面讲解了 Executor框架以及 ThreadPoolExecutor 类,下面让我们实战一下,来通过写一个 ThreadPoolExecutor 的小 Demo 来回顾上面的内容。

4.1 示例代码:Runnable+ThreadPoolExecutor

首先创建一个 Runnable 接口的实现类(当然也可以是 Callable 接口,我们上面也说了两者的区别。)

MyRunnable.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pool-1-thread-3 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:37 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-5 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-4 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-3 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-2 Start. Time = Sun Apr 12 11:14:42 CST 2020
pool-1-thread-1 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-4 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-5 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-3 End. Time = Sun Apr 12 11:14:47 CST 2020
pool-1-thread-2 End. Time = Sun Apr 12 11:14:47 CST 2020

4.2 线程池原理分析

承接 4.1 节,我们通过代码输出结果可以看出:线程首先会先执行 5 个任务,然后这些任务有任务被执行完的话,就会去拿新的任务执行。 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)

现在,我们就分析上面的输出内容来简单分析一下线程池原理。

为了搞懂线程池的原理,我们需要首先分析一下 execute方法。 在 4.1 节中的 Demo 中我们使用 executor.execute(worker)来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 存放线程池的运行状态 (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 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。

图解线程池实现原理

addWorker 这个方法主要用来创建新的工作线程,如果返回true说明创建和启动工作线程成功,否则的话返回的就是false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
 // 全局锁,并发操作必备
private final ReentrantLock mainLock = new ReentrantLock();
// 跟踪线程池的最大大小,只有在持有全局锁mainLock的前提下才能访问此集合
private int largestPoolSize;
// 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁mainLock的前提下才能访问此集合
private final HashSet<Worker> workers = new HashSet<>();
//获取线程池状态
private static int runStateOf(int c) { return c & ~CAPACITY; }
//判断线程池的状态是否为 Running
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}


/**
* 添加新的工作线程到线程池
* @param firstTask 要执行
* @param core参数为true的话表示使用线程池的基本大小,为false使用线程池最大大小
* @return 添加成功就返回true否则返回false
*/
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
//这两句用来获取线程池的状态
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
//获取线程池中线程的数量
int wc = workerCountOf(c);
// core参数为true的话表明队列也满了,线程池大小变为 maximumPoolSize
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
//原子操作将workcount的数量加1
if (compareAndIncrementWorkerCount(c))
break retry;
// 如果线程的状态改变了就再次执行上述操作
c = ctl.get();
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 标记工作线程是否启动成功
boolean workerStarted = false;
// 标记工作线程是否创建成功
boolean workerAdded = false;
Worker w = null;
try {

w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
// 加锁
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//获取线程池状态
int rs = runStateOf(ctl.get());
//rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中
//(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
// firstTask == null证明只新建线程而不执行任务
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
//更新当前工作线程的最大容量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
// 工作线程是否启动成功
workerAdded = true;
}
} finally {
// 释放锁
mainLock.unlock();
}
//// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
if (workerAdded) {
t.start();
/// 标记线程启动成功
workerStarted = true;
}
}
} finally {
// 线程启动失败,需要从工作线程中移除对应的Worker
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

更多关于线程池源码分析的内容推荐这篇文章:《JUC线程池ThreadPoolExecutor源码分析

现在,让我们在回到 4.1 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?

没搞懂的话,也没关系,可以看看我的分析:

我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 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

1
2
3
4
5
6
7
@FunctionalInterface
public interface Runnable {
/**
* 被线程执行,没有返回值也无法抛出异常
*/
public abstract void run();
}

Callable.java

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法这样做时抛出异常。
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}

4.3.2 execute() vs submit()

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功 ,并且可以通过 Futureget()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

我们以**AbstractExecutorService**接口中的一个 submit 方法为例子来看看源代码:

1
2
3
4
5
6
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

上面方法调用的 newTaskFor 方法返回了一个 FutureTask 对象。

1
2
3
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

我们再来看看execute()方法:

1
2
3
public void execute(Runnable command) {
...
}

4.3.3 shutdown()VSshutdownNow()

  • shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。
  • shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List。

4.3.2 isTerminated() VS isShutdown()

  • isShutDown 当调用 shutdown() 方法后返回为 true。
  • isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true

4.4 加餐:Callable+ThreadPoolExecutor示例代码

MyCallable.java

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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:

1
2
3
4
5
6
7
8
9
10
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 类中的相关源代码来看一下相关实现:

1
2
3
4
5
6
7
8
9
/**
* 创建一个可重用固定数量线程的线程池
*/
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

另外还有一个 FixedThreadPool 的实现方法,和上面的类似,所以这里不多做阐述:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

从上面源代码可以看出新创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 都被设置为 nThreads,这个 nThreads 参数是我们使用的时候自己传递的。

5.1.2 执行任务过程介绍

FixedThreadPoolexecute() 方法运行示意图(该图片来源:《Java 并发编程的艺术》):

FixedThreadPool的execute(../../../JavaGuide/docs/java/multi-thread/images/java线程池学习总结/FixedThreadPool.png)方法运行示意图

上图说明:

  1. 如果当前运行的线程数小于 corePoolSize, 如果再来新任务的话,就创建新的线程来执行任务;
  2. 当前运行的线程数等于 corePoolSize 后, 如果再来新任务的话,会将任务加入 LinkedBlockingQueue
  3. 线程池中的线程执行完 手头的任务后,会在循环中反复从 LinkedBlockingQueue 中获取任务来执行;

5.1.3 为什么不推荐使用FixedThreadPool

FixedThreadPool 使用无界队列 LinkedBlockingQueue(队列的容量为 Intger.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize;
  2. 由于使用无界队列时 maximumPoolSize 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 FixedThreadPool的源码可以看出创建的 FixedThreadPoolcorePoolSizemaximumPoolSize 被设置为同一个值。
  3. 由于 1 和 2,使用无界队列时 keepAliveTime 将是一个无效参数;
  4. 运行中的 FixedThreadPool(未执行 shutdown()shutdownNow())不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。

5.2 SingleThreadExecutor 详解

5.2.1 介绍

SingleThreadExecutor 是只有一个线程的线程池。下面看看SingleThreadExecutor 的实现:

1
2
3
4
5
6
7
8
9
10
/**
*返回只有一个线程的线程池
*/
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

从上面源代码可以看出新创建的 SingleThreadExecutorcorePoolSizemaximumPoolSize 都被设置为 1.其他参数和 FixedThreadPool 相同。

5.2.2 执行任务过程介绍

SingleThreadExecutor 的运行示意图(该图片来源:《Java 并发编程的艺术》):
SingleThreadExecutor的运行示意图

上图说明;

  1. 如果当前运行的线程数少于 corePoolSize,则创建一个新的线程执行任务;
  2. 当前线程池中有一个运行的线程后,将任务加入 LinkedBlockingQueue
  3. 线程执行完当前的任务后,会在循环中反复从LinkedBlockingQueue 中获取任务来执行;

5.2.3 为什么不推荐使用SingleThreadExecutor

SingleThreadExecutor 使用无界队列 LinkedBlockingQueue 作为线程池的工作队列(队列的容量为 Intger.MAX_VALUE)。SingleThreadExecutor 使用无界队列作为线程池的工作队列会对线程池带来的影响与 FixedThreadPool 相同。说简单点就是可能会导致 OOM,

5.3 CachedThreadPool 详解

5.3.1 介绍

CachedThreadPool 是一个会根据需要创建新线程的线程池。下面通过源码来看看 CachedThreadPool 的实现:

1
2
3
4
5
6
7
8
9
10
/**
* 创建一个线程池,根据需要创建新线程,但会在先前构建的线程可用时重用它。
*/
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

CachedThreadPoolcorePoolSize 被设置为空(0),maximumPoolSize被设置为 Integer.MAX.VALUE,即它是无界的,这也就意味着如果主线程提交任务的速度高于 maximumPool 中线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

5.3.2 执行任务过程介绍

CachedThreadPool 的 execute()方法的执行示意图(该图片来源:《Java 并发编程的艺术》):
CachedThreadPool的execute(../../../JavaGuide/docs/java/multi-thread/images/java线程池学习总结/CachedThreadPool-execute.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 主要用来在给定的延迟后运行任务,或者定期执行任务。 这个在实际项目中基本不会被用到,因为有其他方案选择比如quartz。大家只需要简单了解一下它的思想。关于如何在 Spring Boot 中 实现定时任务,可以查看这篇文章《5 分钟搞懂如何在 Spring Boot 中 Schedule Tasks》

6.1 简介

ScheduledThreadPoolExecutor 使用的任务队列 DelayQueue 封装了一个 PriorityQueuePriorityQueue 会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTasktime 变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTasksquenceNumber 变量小的先执行)。

ScheduledThreadPoolExecutorTimer 的比较:

  • Timer 对系统时钟的变化敏感,ScheduledThreadPoolExecutor不是;
  • Timer 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 ScheduledThreadPoolExecutor 可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程;
  • TimerTask 中抛出的运行时异常会杀死一个线程,从而导致 Timer 死机:-( …即计划任务将不再运行。ScheduledThreadExecutor 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 afterExecute 方法ThreadPoolExecutor)。抛出异常的任务将被取消,但其他任务将继续运行。

综上,在 JDK1.5 之后,你没有理由再使用 Timer 进行任务调度了。

备注: Quartz 是一个由 java 编写的任务调度库,由 OpenSymphony 组织开源出来。在实际项目开发中使用 Quartz 的还是居多,比较推荐使用 Quartz。因为 Quartz 理论上能够同时对上万个任务进行调度,拥有丰富的功能特性,包括任务调度、任务持久化、可集群化、插件等等。

6.2 运行机制

ScheduledThreadPoolExecutor运行机制

ScheduledThreadPoolExecutor 的执行主要分为两大部分:

  1. 当调用 ScheduledThreadPoolExecutorscheduleAtFixedRate() 方法或者 scheduleWithFixedDelay() 方法时,会向 ScheduledThreadPoolExecutorDelayQueue 添加一个实现了 RunnableScheduledFuture 接口的 ScheduledFutureTask
  2. 线程池中的线程从 DelayQueue 中获取 ScheduledFutureTask,然后执行任务。

ScheduledThreadPoolExecutor 为了实现周期性的执行任务,对 ThreadPoolExecutor做了如下修改:

  • 使用 DelayQueue 作为任务队列;
  • 获取任务的方不同
  • 执行周期任务后,增加了额外的处理

6.3 ScheduledThreadPoolExecutor 执行周期任务的步骤

ScheduledThreadPoolExecutor执行周期任务的步骤

  1. 线程 1 从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask的 time 大于等于当前系统的时间;
  2. 线程 1 执行这个 ScheduledFutureTask
  3. 线程 1 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
  4. 线程 1 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

七 线程池大小确定

线程池数量的确定一直是困扰着程序员的一个难题,大部分程序员在设定线程池大小的时候就是随心而定。

很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了上下文切换成本。不清楚什么是上下文切换的话,可以看我下面的介绍。

上下文切换:

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。

如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。

但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。

有一个简单并且适用面比较广的公式:

  • CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
  • I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。

如何判断是 CPU 密集任务还是 IO 密集任务?

CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。

八 参考

九 其他推荐阅读

转载自Java Guide https://github.com/Snailclimb/JavaGuide

前言

目录在日常开发中是一种非常常见的需求,今天我们来详细讨论下一种实现方案,以及查询思路。

设计思路

目录其实就是一种树形结构,子目录与父目录形成一种父子关系,所以很自然 的想到维护他们这种关系就能实现了。

数据库设计

1
2
3
4
5
6
create table tree_node
(
id bigint not null comment '主键id'
primary key,
parent bigint null comment '父节点(root节点为0)'
)
  • parent存储父子关系
  • 这种设计很直观,也最容易理解,专业术语成为邻接表
  • 唯一的缺点是在做修改、删除的时候比较麻烦,建议在确定结构后不要再修改目录结构

树节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class TreeNode<T> {

/**
* 节点ID
*/
private Long id;

/**
* 节点数据
*/
private T date;

/**
* 节点父节点ID
*/
private Long parent;

/**
* 孩子节点
*/
private List<TreeNode<T>> child;

public void addChild(TreeNode<T> node) {
if (CollectionUtils.isEmpty(this.child)) {
this.child = new ArrayList<>();
}
this.child.add(node);
}

// Get Set ...
}

常见需求

生成树

根据节点列表生成树

  • 给列表根据id建立索引
  • 遍历列表,将自己加入父节点的孩子列表, 形成父子关系
  • 建立一个ROOT节点作为根节点,将一级目录放在根节点下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 生成树
*
* @param nodes 所有节点
* @return tree
*/
public static <T> TreeNode<T> tree(List<TreeNode<T>> nodes) {
// 建立索引
Map<Long, TreeNode<T>> map = nodes.stream().collect(Collectors.toMap(TreeNode::getId, item -> item));
// ROOT 节点
TreeNode<T> root = new TreeNode<>();
for (TreeNode<T> node : nodes) {
if (node.getParent() == null || node.getParent() == 0) {
root.addChild(node);
} else {
Long parent = node.getParent();
map.get(parent).addChild(node);
}
}
return root;
}

获取树的某个节点

由于是目录设计,这里采用广度优先遍历,优先遍历上层目录

  • 使用队列实现的广度优先遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 获取节点 采用广度优先遍历
*
* @param id 节点id
* @param tree 树
* @return result
*/
public static <T> TreeNode<T> getNode(long id, TreeNode<T> tree) {
Queue<TreeNode<T>> queue = new LinkedBlockingQueue<>();
queue.add(tree);
while (!queue.isEmpty()) {
TreeNode<T> node = queue.poll();
if (Objects.equals(node.getId(), id)) {
return node;
}
if (!CollectionUtils.isEmpty(node.getChild())) {
queue.addAll(node.getChild());
}
}
return null;
}

获取所有节点

有时候需要获取某个节点下面的全部节点信息,例如在做搜索的时候就有这个需求

  • 这里采用深度优先遍历所有节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 深度优先遍历获取所有节点
*
* @param tree 树
* @param list 节点列表
*/
public static <T> void getAllTreeNode(TreeNode<T> tree, List<TreeNode<T>> list) {
list.add(tree);
List<TreeNode<T>> child = tree.getChild();
if (!CollectionUtils.isEmpty(child)) {
for (TreeNode<T> node : child) {
getAllTreeNode(node, list);
}
}
}

前言

最近在读JDK源码,发现HashMap里面存再大量的逻辑运算,而Java逻辑运算其实在我实际的开发当中用的相当的少,有些甚至还看不太懂,下面记录一些我读源码的时候遇到的一些逻辑运算。

>> 和 >>>

  • >>右移位符,该符号表示将二进制右移一位,不带符号位

  • >>>带符号右移位符,该符号表示将二进制右移一位,带符号位

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class JavaLogic {
public static void main(String[] args) {
int a = -10;
outBinaryString(a);
outBinaryString(a >> 1);
outBinaryString(a >>> 1);
}

public static void outBinaryString(int a) {
System.out.printf("%20d --> ", a);
print(a);
System.out.println();
}

public static void print(int num) {
for (int i = 31; i >= 0; i--) {
System.out.print((num & (1 << i)) == 0 ? "0" : "1");
}
}
}

运行Main方法打印出如下结果

1
2
3
       -10 --> 11111111111111111111111111110110
-5 --> 11111111111111111111111111111011
2147483643 --> 01111111111111111111111111111011

相关源码

java.util.HashMap#hash

1
2
3
4
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

文章将在我的Git Page更新 原文链接

前言

大家开发中用得最多的工具类就是JAVA的集合框架了,今天我们来从源码的角度剖析Map集合的实现之一HashMap

HashMap 简介

这里我就直接翻译JDK源码注释了,其实注释讲得很详细了。

基于哈希表的Map接口的实现。 此实现提供所有可选的映射操作,并允许空值和空键。 ( HashMap类与Hashtable大致等效,不同之处在于它是不同步的,并且允许为null。)此类不保证映射的顺序。 特别是,它不能保证顺序会随着时间的推移保持恒定。
假设哈希函数将元素正确分散在存储桶中,则此实现为基本操作( get和put )提供恒定时间(这里指时间复杂度为o1)的性能。 集合视图上的迭代所需的时间与HashMap实例的“容量”(存储桶数)及其大小(键-值映射数)成正比。 因此,如果迭代性能很重要,则不要将初始容量设置得过高(或负载因数过低),这一点非常重要。
HashMap的实例具有两个影响其性能的参数:初始容量和负载因子。 容量是哈希表中存储桶的数量,初始容量只是创建哈希表时的容量。 负载因子是在自动增加其哈希表容量之前允许哈希表获得的满度的度量。 当哈希表中的条目数超过负载因子和当前容量的乘积时,哈希表将被重新哈希(即,内部数据结构将被重建),因此哈希表的存储桶数约为两倍。
通常,默认负载因子(.75)在时间和空间成本之间提供了一个很好的权衡。 较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put )。 设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的次数。 如果初始容量大于最大条目数除以负载因子,则将不会发生任何哈希操作。
如果将许多映射存储在HashMap实例中,则创建具有足够大容量的映射将比让其根据需要增长表的自动重新哈希处理更有效地存储映射。 请注意,使用具有相同hashCode()许多键是降低任何哈希表性能的肯定方法。 为了改善影响,当键为Comparable ,此类可以使用键之间的比较顺序来帮助打破平局。
请注意,此实现未同步。 如果多个线程同时访问哈希映射,并且至少有一个线程在结构上修改该映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已经包含的键相关联的值不是结构修改。)通常通过在自然封装了Map的某个对象上进行同步来实现。 。 如果不存在这样的对象,则应使用Collections.synchronizedMap方法“包装”Map。 最好在创建时完成此操作,以防止意外不同步地访问Map:
Map m = Collections.synchronizedMap(new HashMap(…));
该类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在创建迭代器后的任何ff时间以任何方式对Map进行结构修改,则除了通过迭代器自己的remove方法之外,迭代器都会抛出ConcurrentModificationException 。 因此,面对并发修改,迭代器会快速干净地失败,而不会在未来的不确定时间冒着任意,不确定的行为的风险。
请注意,迭代器的快速失败行为无法得到保证,因为通常来说,在存在不同步的并发修改的情况下,不可能做出任何严格的保证。 快速失败的迭代器会尽最大努力抛出ConcurrentModificationException 。 因此,编写依赖于此异常的程序的正确性是错误的:迭代器的快速失败行为应仅用于检测错误。
此类是Java Collections Framework的成员

几个重要的成员变量

  • Node<K,V>[] table 核心数据结构 数组 + 链表
  • int size 集合的大小
  • int modCount 被修改的次数
  • int threshold 下一个要调整大小的大小值(容量 * 负载因子)
  • float loadFactor 负载因子
  • Set<Map.Entry<K,V>> entrySet KV集

方法

构造方法

HashMap 有多个重载构造方法,但最终都会去掉下面这个构造方法

1
2
3
4
5
6
7
8
9
10
11
12
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

有两个参数

  • initialCapacity 初始容量
  • loadFactor 负载因子

HashMap就是对散列表这种数据结构的实现,所以需要这个两个参数去定义散列表

tableSizeFor

我们从上面的构造方法可以看出,HashMap在初始化的时候,会调用这个方法去计算实际初始化的容量并暂存为threshold

1
2
3
4
5
6
7
8
9
10
11
12
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

这个方法返回大于输入参数且最近的2的整数次幂的数。比如10,则返回16。

先来分析有关n位操作部分:先来假设n的二进制为01xxx…xxx。接着

  • 对n右移1位:001xx…xxx,再位或:011xx…xxx

  • 对n右移2为:00011…xxx,再位或:01111…xxx

  • 此时前面已经有四个1了,再右移4位且位或可得8个1

  • 同理,有8个1,右移8位肯定会让后八位也为1。

综上可得,该算法让最高位的1后面的位全变为1。

最后再让结果n+1,即得到了2的整数次幂的值了。

现在回来看看第一条语句:

1
int n = cap - 1;

cap - 1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。

这种方法的效率非常高,可见Java8对容器优化了很多

hash

1
2
3
4
5
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

这里是取hash的高16位与hash值进行异或运算,因为在进行槽位计算的时候容易丢失高位的特征,所以采取低位与高位进行运算获得一个结果,保留了高位与低位的特征。如果采用|将会使位偏向1,如果采用&将会偏向0,而采用^则没有明显的偏向性。

槽位计算源码

1
tab[i = (n - 1) & hash]

PUT

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

这个方法将元素加入散列表,具体实现是下面这个putVal方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* Implements Map.put and related methods.
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

明确几个局部变量的意义

  • Node<K,V>[] tab 散列表
  • Node<K,V> p 新增的节点
  • int n 散列表数组的长度
  • int i 计算出的槽位

我们跟随源码的流程进行分析

首先判断散列表是否有被创建出来,如果没有创建,则进行散列表的初始化。这是一种懒加载策略。初始化的过程我们下面再分析。

然后就是计算散列位置,判断该位置上是否有元素。若没有则直接把元素放入。如果该节点上有元素了,则发生了hash碰撞,需要另外的处理。

下面分析一下槽点计算的源码

1
i = (n - 1) & hash

将容量-1与hash值进行与运算。由于散列表在初始化的时候,容量是2的次幂,所以在减一之后,二进制位都变为了1,再与hash值进行与运算其实就是取余数,这个就相当于hash % n 。但是效率确比取模运算高出很多。

如果发生了hash碰撞,则首先会进行key是否一致导致的,如果key不等发生的hash碰撞,则会吧当前元素放在链表的下一个节点,如果链表长度超过8,则会树化处理,变为红黑树。

image-20210126190839451

一个小插件,可以使你的括号变色。

既美观,也可以避免一些错误。

我表示相见恨晚

附上插件首页

https://plugins.jetbrains.com/plugin/10080-rainbow-brackets

举个栗子

我们来看下这个方法的源码java.util.Map#getOrDefault

1
2
3
4
5
6
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}

这括号是不是看得人头都晕掉了,安装彩虹括号后就变得很清晰了

image-20210129153934682

通过颜色标注,关系就很清晰了。

前言

上篇文章讲到了XML配置方式在不指定ID的情况下,Spring的BeanName。今天来谈论下,现代Spring中Bean注册使用最多的方式—注解。注解方式生成的BeanName策略又是怎样的呢。

Spring 常用注册Bean注解

  • @Component@Service@Repository@Controller 这四个注解用于类上,实质上是一样的,能够注册当前类到容器,value属性就是BeanName
  • @Configuration这个注解同样用作类上,不同的是,这个注解通常与@Bean配合使用,注册方法的返回类型对象,用作配置。
  • @Bean 用于方法上,该方法需要在@Configuration标注的类里面,且方法必须为public

AnnotationBeanNameGenerator

@Component@Service@Repository@Controller@Configuration

这五个注解注册的Bean名称都由AnnotationBeanNameGenerator生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class AnnotationBeanNameGenerator implements BeanNameGenerator {

/**
* A convenient constant for a default {@code AnnotationBeanNameGenerator} instance,
* as used for component scanning purposes.
* @since 5.2
*/
public static final AnnotationBeanNameGenerator INSTANCE = new AnnotationBeanNameGenerator();

private static final String COMPONENT_ANNOTATION_CLASSNAME = "org.springframework.stereotype.Component";


@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
if (definition instanceof AnnotatedBeanDefinition) {
String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);
if (StringUtils.hasText(beanName)) {
// Explicit bean name found.
return beanName;
}
}
// Fallback: generate a unique default bean name.
return buildDefaultBeanName(definition, registry);
}
}

该生成器同样实现了父类BeanNameGeneratorgenerateBeanName方法,与默认实现不同的是没有委托给其它类实现功能,而在自身实现,同样存在一个单列对象。

可以看到命名逻辑先是从注解的元信息获取配置的BeanName,在获取不到的情况下回去调用buildDefaultBeanName生成一个名称。

下面我们看下生成逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

/**
* Derive a default bean name from the given bean definition.
* <p>The default implementation delegates to {@link #buildDefaultBeanName(BeanDefinition)}.
* @param definition the bean definition to build a bean name for
* @param registry the registry that the given bean definition is being registered with
* @return the default bean name (never {@code null})
*/
protected String buildDefaultBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
return buildDefaultBeanName(definition);
}

/**
* Derive a default bean name from the given bean definition.
* <p>The default implementation simply builds a decapitalized version
* of the short class name: e.g. "mypackage.MyJdbcDao" -> "myJdbcDao".
* <p>Note that inner classes will thus have names of the form
* "outerClassName.InnerClassName", which because of the period in the
* name may be an issue if you are autowiring by name.
* @param definition the bean definition to build a bean name for
* @return the default bean name (never {@code null})
*/
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
Assert.state(beanClassName != null, "No bean class name set");
String shortClassName = ClassUtils.getShortName(beanClassName);
return Introspector.decapitalize(shortClassName);
}

该方法委托给另外一个重载方法实现,先获取该Bean的ClassName,然后获取短的命称,即类名,最后decapitalize把类名首字母变为小写。

使用采用这种方式生成的BeanName就是类名首字母小写。例如com.oneyoung.User -> user

@Bean 名称生成策略

使用@Bean方式注册到容器的命名方式与上面有所不同。

加载实现类是ConfigurationClassBeanDefinitionReader专门用来处理配置类的Bean

1
2
3
4
5
6
AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
Assert.state(bean != null, "No @Bean annotation attributes");

// Consider name and any aliases
List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));
String beanName = (!names.isEmpty() ? names.remove(0) : methodName);

这部分代码可以看出,先去注解的value配置,如果为空则取方法名。所以默认的beanName就是方法名。

前言

Spring在进行Bean注册的时候需要有一个唯一标识,这个标识也成为BeanName。在进行Bean注册时,这个id并不必填项,它可以由Spring自动生成。下面就来讨论其中一种生成策略。

BeanNameGenerator

org.springframework.beans.factory.support.BeanNameGenerator这个接口就是Spring Bean名称生成器。我们看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface BeanNameGenerator {

/**
* Generate a bean name for the given bean definition.
* @param definition the bean definition to generate a name for
* @param registry the bean definition registry that the given definition
* is supposed to be registered with
* @return the generated bean name
*/
String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry);

}

该接口只定义了一个方法用于获取BeanName,需要传入BeanDefinition BeanDefinitionRegistry

这里简单说明下为什么需要这两个参数

  • BeanDefinition用于获取该Bean的ClassName
  • BeanDefinitionRegistry用于判断生成的BeanName是否已注册,如果已注册会附加类似#0这样的序列号

BeanNameGenerator 实现类

这个接口有两个实现 DefaultBeanNameGenerator AnnotationBeanNameGenerator

这里我们暂时只讨论前者,这个名称生成器的默认实现

DefaultBeanNameGenerator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* Default implementation of the {@link BeanNameGenerator} interface, delegating to
* {@link BeanDefinitionReaderUtils#generateBeanName(BeanDefinition, BeanDefinitionRegistry)}.
*
* @author Juergen Hoeller
* @since 2.0.3
*/
public class DefaultBeanNameGenerator implements BeanNameGenerator {

/**
* A convenient constant for a default {@code DefaultBeanNameGenerator} instance,
* as used for {@link AbstractBeanDefinitionReader} setup.
* @since 5.2
*/
public static final DefaultBeanNameGenerator INSTANCE = new DefaultBeanNameGenerator();


@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
return BeanDefinitionReaderUtils.generateBeanName(definition, registry);
}

}

这个默认实现类,存在一个单例对象,看doc是5.2才加的,因为要兼容之前的版本,所以没有私有化构造器

该实现类,委托给BeanDefinitionReaderUtils实现功能

BeanDefinitionReaderUtils

真正的实现是这个工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static String generateBeanName(
BeanDefinition definition, BeanDefinitionRegistry registry, boolean isInnerBean)
throws BeanDefinitionStoreException {

String generatedBeanName = definition.getBeanClassName();
if (generatedBeanName == null) {
if (definition.getParentName() != null) {
generatedBeanName = definition.getParentName() + "$child";
}
else if (definition.getFactoryBeanName() != null) {
generatedBeanName = definition.getFactoryBeanName() + "$created";
}
}
if (!StringUtils.hasText(generatedBeanName)) {
throw new BeanDefinitionStoreException("Unnamed bean definition specifies neither " +
"'class' nor 'parent' nor 'factory-bean' - can't generate bean name");
}

String id = generatedBeanName;
if (isInnerBean) {
// Inner bean: generate identity hashcode suffix.
id = generatedBeanName + GENERATED_BEAN_NAME_SEPARATOR + ObjectUtils.getIdentityHexString(definition);
}
else {
// Top-level bean: use plain class name with unique suffix if necessary.
return uniqueBeanName(generatedBeanName, registry);
}
return id;
}

这里可以看出id是直接取的Bean的ClassName,如果是非内部类,会做唯一性校验

1
2
3
4
5
6
7
8
9
10
11
12
public static String uniqueBeanName(String beanName, BeanDefinitionRegistry registry) {
String id = beanName;
int counter = -1;

// Increase counter until the id is unique.
while (counter == -1 || registry.containsBeanDefinition(id)) {
counter++;
id = beanName + GENERATED_BEAN_NAME_SEPARATOR + counter;
}
return id;
}

每个beanName都会被加上#0,然后去容器里面看是否被注册,如果被组成则序号加一

写得比较粗糙,有时间会详细写一下

前言

单例模式是 Java 设计模式中最简单的一种,只需要一个类就能实现单例模式,但是,你可不能小看单例模式,虽然从设计上来说它比较简单,但是在实现当中你会遇到非常多的坑,所以,系好安全带,上车。

什么是单例模式

单例模式(Singleton)就是一个类在程序运行中只实例化一次,创建一个全局唯一对象,有点像 Java 的静态变量,但是单例模式要优于静态变量,静态变量在程序启动的时候JVM就会进行加载,如果不使用,会造成大量的资源浪费。单例模式能够实现懒加载,能够在使用实例的时候才去创建实例。开发工具类库中的很多工具类都应用了单例模式,比如线程池、缓存、日志对象等,它们都只需要创建一个对象。

在开发中,会经常遇到一个全局使用的类频繁地创建与销毁,这会非常浪费系统的内存资源,而且容易导致错误甚至一定会产生错误,所以我们单例模式所期待的目标或者说使用它的目的,是为了尽可能的节约内存空间,减少无谓的GC消耗,并且使应用可以正常运作。

如何实现单例模式

  • 静态并私有化实例对象
  • 提供一个公共的静态方法访问私有的静态实例,用来返回唯一实例对象
  • 私有化构造方法,禁止通过构造方法创建实例

单例有什么好处

  • 只有一个对象,内存开支少、性能好
  • 避免对资源的多重占用
  • 在系统设置全局访问点,优化和共享资源访问

以下介绍常见的几种单例模式实现方式

单例模式的写法有饿汉模式、懒汉模式、双重检查锁模式、静态内部类单例模式、枚举类实现单例模式五种方式,其中懒汉模式、双重检查锁模式,如果你写法不当,在多线程情况下会存在不是单例或者单例出异常等问题,具体的原因,在后面的对应处会进行说明。我们从最基本的饿汉模式开始我们的单例编写之路。

饿汉模式

所谓饿汉,自然是非常迫切获取食物,这里的食物所指的自然就是我们的单例对象了。饿汉模式是已一种简单粗暴的方式创建单例对象,在定义静态属性时,直接实例化了对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SingletonObjectHunger {
/**
* 私有静态实例
*/
private static final SingletonObjectHunger INSTANCE = new SingletonObjectHunger();

/**
* 私有化构造函数
*/
private SingletonObjectHunger(){
// code
}

/**
* 提供公开获取实例接口
* @return 单例对象
*/
public static SingletonObjectHunger getInstance(){
return INSTANCE;
}
}

优点

  • 由于使用了static关键字,保证了在引用这个变量时,关于这个变量的所以写入操作都完成,所以保证了JVM层面的线程安全

缺点

  • 没有实现懒加载的效果,如果一个类比较大,我们在初始化的时就加载了这个类,但是如果我们没有使用这个类,这就导致了内存空间的浪费。

懒汉模式

所谓懒汉,就是以偷懒的方式创建单例。这种方式在类初始化的时候不会创建实例,只有在获取使用实例的时候才会创建实例,这样就解决了饿汉模式的空间浪费问题,但是也引入了其他问题。下面介绍一种懒汉模式的写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SingletonObjectLazy {

/**
* 私有静态实例 未初始化实例
*/
private static SingletonObjectLazy instance;

/**
* 私有化构造函数
*/
private SingletonObjectLazy(){
// code
}

/**
* 提供公开获取实例接口
* @return 单例对象
*/
public static SingletonObjectLazy getInstance(){
// 先判断实例是否为空,如果实例为空,则实例化对象
if (instance == null) {
instance = new SingletonObjectLazy();
}
return instance;
}
}

上面是一种懒汉模式的实现方式,但在多线程情况是线程不安全的。这种写法保证不了单列模式,可能出现多实例的情况。下面分析出现多实例的原因

1
2
1 if (instance == null) {
2 instance = new SingletonObjectLazy();

问题就出在上面这个代码片段内。假设有两个线程同时进入到 1 这个位置,因为没有任何资源保护措施,所以两个线程可以同时判断的instance都为空,都将去执行 2 的实例化代码,这样就实例化了两份实例,所以会出现多份实例的情况。

通过上面的分析我们已经知道出现多份实例的原因,如果我们在创建实例的时候进行资源保护,不就可以解决多份实例的问题了吗?确实如此,我们给getInstance()方法加上synchronized关键字,使得getInstance()方法成为同步方法,能够解决多份实例的问题。加上synchronized关键字之后代码如下:

1
public synchronized static SingletonObjectLazy getInstance(){

这样,在某一时刻永远只会有一个线程能执行这个方法,初始化完毕后,其他线程直接获取这个实例。这样似乎解决了线程安全问题。但是同样引入了一个新的问题,加锁之后会使得程序变成串行化,只有抢到锁的线程才能去执行这段代码块,这会使得系统的并发性能大大下降。

优点

  • 避免了饿汉模式的缺点,实现了懒加载,节约了内存空间

缺点

  • 在不加锁的情况下,线程不安全,可能出现多份实例
  • 在加锁的情况下,使程序串行化,导致系统存在严重并发性能问题

双重检查锁模式

再来讨论一下懒汉模式中加锁的问题,对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要去除这个同步锁。由此也产生了一种新的实现模式:双重检查锁模式。这种模式完美解决了上面的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class SingletonObjectCheck {
/**
* 定义静态变量时,未初始化实例
* 要解决双重检查锁模式带来空指针异常的问题,只需要使用volatile关键字,volatile关键字严格遵循happens-before原则,即在读操作前,写操作必须全部完成。
*/
private volatile static SingletonObjectCheck instance;

/**
* 私有化构造函数
*/
private SingletonObjectCheck(){

}

public static SingletonObjectCheck getInstance(){
// 使用时,先判断实例是否为空,如果实例为空,则实例化对象
if (instance == null) {
// 使用互斥锁 只允许单一线程初始化实例
synchronized (SingletonObjectCheck.class){
if (instance == null) {
instance = new SingletonObjectCheck();
}
}
}
return instance;
}
}

这种写法在去除了方法上的同步锁,采用互斥锁去创建实例。假设同时有两个线程通过了第一次检查,进入到了互斥锁,线程A获取到这个锁,线程B则阻塞等待。线程A执行初始化对象后,释放锁。线程B获得锁,由于有二次检查,实例已初始化就不会去再次初始化对象。

扩展:上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排操作。如果构造函数中操作比较多时,为了提升效率,JVM 会在构造函数里面的属性未全部完成实例化时,就返回对象。双重检测锁出现空指针问题的原因就是出现在这里,当某个线程获取锁进行实例化时,其他线程就直接获取实例使用,由于JVM指令重排序的原因,其他线程获取的对象也许不是一个完整的对象,所以在使用实例的时候就会出现空指针异常问题。

关于指令重排,感兴趣的同学可以自行了解。

添加volatile关键字之后的双重检查锁模式就比较完美了,能够保证在多线程的情况下线程安全也不会有性能问题。

静态内部类单例模式

静态内部类不依赖外部类,是一种很特殊的内部类。在创建静态内部类的时候,不需要外部类对象的引用。你可以把它当做顶级类。

静态内部类单例模式也称单例持有者模式,实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有静态内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由static修饰,保证只被实例化一次,并且严格保证实例化顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingletonObjectStaticInner {

/**
* 私有化构造方法
*/
private SingletonObjectStaticInner() {
}

/**
* 单列持有者
*/
private static class InstanceHolder{
private static final SingletonObjectStaticInner INSTANCE = new SingletonObjectStaticInner();
}

public static SingletonObjectStaticInner getInstance(){
return InstanceHolder.INSTANCE;
}
}

这里静态内部类为私有,只有这个外部类能访问。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

枚举类实现单例模式

枚举类实现单例模式是 effective java 作者极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class SingletonObjectEnum {

/**
* 私有化构造函数
*/
private SingletonObjectEnum() {
}

/**
* 定义一个静态枚举类
*/
static enum SingletonEnum {
/**
* 创建一个枚举对象,该对象天生为单例
*/
INSTANCE;
private SingletonObjectEnum SingletonObjectEnum;

/**
* 私有化枚举的构造函数
*/
private SingletonEnum() {
SingletonObjectEnum = new SingletonObjectEnum();
}

public SingletonObjectEnum getInstance() {
return SingletonObjectEnum;
}
}

public static SingletonObjectEnum getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
}

这里的静态枚举内部类与之前的静态内部类都是静态内部类,不依赖外部类。枚举值天生为单例,保证了实例的唯一性,私有化枚举构造函数,阻止再次实例化。

扩展:破坏单例模式的方法及解决办法

  • 除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例,解决办法如下:
1
2
3
4
5
private SingletonObject(){
if (instance !=null){
throw new RuntimeException("实例已经存在,请勿重复初始化");
}
}
  • 如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
1
2
3
public Object readResolve() throws ObjectStreamException {
return instance;
}

Elasticsearch introduction

Index some documents

集群启动并运行后,您就可以为一些数据建立索引了。 Elasticsearch有多种摄取选项,但最终它们都做同样的事情:将JSON文档放入Elasticsearch索引中。

您可以使用简单的PUT请求直接执行此操作,该请求指定要添加文档的索引,唯一的文档ID,以及请求正文中的一个或多个"field": "value"对:

1
2
3
4
PUT /customer/_doc/1
{
"name": "John Doe"
}

该请求(如果尚不存在)将自动创建customer索引,添加ID为1的新文档,并存储name字段并为其建立索引。

由于这是一个新document,因此响应显示该操作的结果是创建了该document的版本1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index" : "customer",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}

可以从群集中的任何节点立即获得新文档。您可以使用指定其文档ID的GET请求检索它:

1
GET /customer/_doc/1

该响应表明找到了具有指定ID的document,并显示了已建立索引的原始源字段。

1
2
3
4
5
6
7
8
9
10
11
12
{
"_index" : "customer",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"name" : "John Doe"
}
}

如果要索引的文档很多,则可以使用bulk API批量提交它们。 使用批量处理批处理文档操作比单独提交请求要快得多,因为它可以最大程度地减少网络往返次数。

最佳批处理大小取决于许多因素:文档大小和复杂性,索引编制和搜索负载以及群集可用的资源。 一个好的起点是批处理1,000至5,000个文档,总有效负载在5MB至15MB之间。 从那里,您可以尝试找到最佳位置。

要将一些数据导入Elasticsearch,您可以开始搜索和分析:

  1. 下载accounts.json示例数据集。 此随机生成的数据集中的文档代表具有以下信息的用户帐户:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    {
    "account_number": 0,
    "balance": 16623,
    "firstname": "Bradshaw",
    "lastname": "Mckenzie",
    "age": 29,
    "gender": "F",
    "address": "244 Columbus Place",
    "employer": "Euron",
    "email": "bradshawmckenzie@euron.com",
    "city": "Hobucken",
    "state": "CO"
    }
  2. 使用以下_bulk请求将帐户数据索引到银行索引中:

    1
    2
    curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@accounts.json"
    curl "localhost:9200/_cat/indices?v"
  3. 响应表明成功索引了1,000个文档。

    1
    2
    health status index uuid                   pri rep docs.count docs.deleted store.size pri.store.size
    yellow open bank l7sSYV2cQXmu6_4rJWVIww 5 1 1000 0 128.6kb 128.6kb

Start searching

将一些数据摄取到Elasticsearch索引后,您可以通过将请求发送到_search端点来进行搜索。要访问全套搜索功能,请使用Elasticsearch Query DSL在请求正文中指定搜索条件。您可以在请求URI中指定要搜索的索引的名称。

例如,以下请求将检索bank索引中按帐号排序的所有文档:

1
2
3
4
5
6
7
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
]
}

默认情况下,响应的hit部分包括符合搜索条件的前10个文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"took" : 63,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value": 1000,
"relation": "eq"
},
"max_score" : null,
"hits" : [ {
"_index" : "bank",
"_type" : "_doc",
"_id" : "0",
"sort": [0],
"_score" : null,
"_source" : {"account_number":0,"balance":16623,"firstname":"Bradshaw","lastname":"Mckenzie","age":29,"gender":"F","address":"244 Columbus Place","employer":"Euron","email":"bradshawmckenzie@euron.com","city":"Hobucken","state":"CO"}
}, {
"_index" : "bank",
"_type" : "_doc",
"_id" : "1",
"sort": [1],
"_score" : null,
"_source" : {"account_number":1,"balance":39225,"firstname":"Amber","lastname":"Duke","age":32,"gender":"M","address":"880 Holmes Lane","employer":"Pyrami","email":"amberduke@pyrami.com","city":"Brogan","state":"IL"}
}, ...
]
}
}

该响应还提供有关搜索请求的以下信息:

  • took – Elasticsearch运行查询所需的时间(以毫秒为单位)
  • timed_out – 搜索请求是否超时
  • _shards – 搜索了多少个分片以及成功,失败或跳过了多少个分片。or were skipped.
  • max_score – 找到最相关文件的分数
  • hits.total.value - 找到多少个匹配的文档
  • hits.sort - 文档的排序位置(不按相关性得分排序时)
  • hits._score - 文档的相关性得分(使用match_all时不适用)

每个搜索请求都是独立的:Elasticsearch在请求中不维护任何状态信息。要翻阅搜索结果,请在请求中指定from和size参数。

例如,以下请求的匹配数为10到19:

1
2
3
4
5
6
7
8
9
GET /bank/_search
{
"query": { "match_all": {} },
"sort": [
{ "account_number": "asc" }
],
"from": 10,
"size": 10
}

既然您已经了解了如何提交基本的搜索请求,则可以开始构建比match_all有趣的查询。

要在字段中搜索特定字词,可以使用match查询。例如,以下请求搜索address字段以查找其地址包含millland

1
2
3
4
GET /bank/_search
{
"query": { "match": { "address": "mill lane" } }
}

要构造更复杂的查询,可以使用bool查询来组合多个查询条件。您可以根据需要(must match),期望(shhould match)或不期望(must not match)指定条件。

例如,以下请求在bank索引中搜索属于40岁客户的帐户,但不包括居住在Idaho(ID)的任何人:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /bank/_search
{
"query": {
"bool": {
"must": [
{ "match": { "age": "40" } }
],
"must_not": [
{ "match": { "state": "ID" } }
]
}
}
}

布尔查询中的每个must,should和must_not元素都称为查询子句。文档满足每个必须或应条款中的标准的程度会提高文档的相关性得分。分数越高,文档就越符合您的搜索条件。默认情况下,Elasticsearch返回按这些相关性分数排名的文档。

must_not子句中的条件被视为过滤器。它影响文件是否包含在结果中,但不会影响文件的评分方式。您还可以根据结构化数据显式指定任意过滤器以包括或排除文档。

例如,以下请求使用范围过滤器将结果限制为余额在20,000美元到30,000美元(含)之间的帐户。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /bank/_search
{
"query": {
"bool": {
"must": { "match_all": {} },
"filter": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
}
}
}
}

Analyze results with aggregations

Elasticsearch聚合使您能够获取有关搜索结果的元信息,并回答诸如“德克萨斯州有多少个帐户持有人?”或“田纳西州的平均帐户余额是多少?”之类的问题。您可以在一个请求中搜索文档,过滤命中并使用汇总分析结果。

例如,以下请求使用terms汇总将bank索引中的所有帐户按状态分组,并按降序返回帐户数量最多的十个州:

1
2
3
4
5
6
7
8
9
10
11
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
}
}
}
}

响应中的bucketsstate字段的值。doc_count显示每个状态下的帐户数。例如,您可以看到ID(爱达荷州)中有27个帐户。因为请求集size=0,所以响应仅包含聚合结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{
"took": 29,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped" : 0,
"failed": 0
},
"hits" : {
"total" : {
"value": 1000,
"relation": "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_state" : {
"doc_count_error_upper_bound": 20,
"sum_other_doc_count": 770,
"buckets" : [ {
"key" : "ID",
"doc_count" : 27
}, {
"key" : "TX",
"doc_count" : 27
}, {
"key" : "AL",
"doc_count" : 25
}, {
"key" : "MD",
"doc_count" : 25
}, {
"key" : "TN",
"doc_count" : 23
}, {
"key" : "MA",
"doc_count" : 21
}, {
"key" : "NC",
"doc_count" : 21
}, {
"key" : "ND",
"doc_count" : 21
}, {
"key" : "ME",
"doc_count" : 20
}, {
"key" : "MO",
"doc_count" : 20
} ]
}
}
}

您可以组合聚合以构建更复杂的数据汇总。例如,以下请求在前一个group_by_state聚合内嵌套avg聚合,以计算每个状态的平均帐户余额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
GET /bank/_search
{
"size": 0,
"aggs": {
"group_by_state": {
"terms": {
"field": "state.keyword"
},
"aggs": {
"average_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
}

除了这些基本的存储桶和指标聚合外,Elasticsearch还提供了专门的聚合,用于在多个字段上操作并分析特定类型的数据,例如日期,IP地址和地理数据。您还可以将单个聚合的结果馈送到管道聚合中,以进行进一步分析。

聚合提供的核心分析功能可启用高级功能,例如使用机器学习来检测异常。

Mapping

映射是定义文档及其包含的字段的存储和索引方式的过程。例如,使用映射定义:

  • 哪些字符串字段应视为全文字段。
  • 哪些字段包含数字,日期或地理位置。
  • format 日期值的格式。
  • 自定义规则来控制动态添加字段的映射。 dynamically added fields.

Field datatypes

每个字段的数据类型可以是:

为不同的目的以不同的方式对同一字段建立索引通常很有用。例如,可以将string字段索引为全文搜索的text字段,以及作为排序或聚合的keyword字段。另外,您可以使用标准分析器,英语分析器和法语分析器为字符串字段建立索引。

这是多字段的目的。大多数数据类型通过fields参数支持多字段。

Dynamic mapping(动态映射)

字段和映射类型在使用之前不需要定义。通过动态映射,仅通过索引文档即可自动添加新的字段名称。新字段既可以添加到顶级映射类型,也可以添加到内部对象和嵌套字段。object and nested

可以将动态映射规则配置为自定义用于新字段的映射。 dynamic mapping

使用显式映射创建索引

您可以使用create index API创建带有显式映射的新索引。

1
2
3
4
5
6
7
8
9
10
PUT /my-index
{
"mappings": {
"properties": {
"age": { "type": "integer" },
"email": { "type": "keyword" },
"name": { "type": "text" }
}
}
}

Add a field to an existing mapping

将字段添加到现有映射 您可以使用放置映射API将一个或多个新字段添加到现有索引。

下面的示例添加employee-id,这是一个关键字字段,其索引映射参数值为false。这意味着已存储employee-id字段的值,但未编制索引或可用于搜索。

1
2
3
4
5
6
7
8
9
PUT /my-index/_mapping
{
"properties": {
"employee-id": {
"type": "keyword",
"index": false
}
}
}

更新字段的映射

您不能更改现有字段的映射,但以下情况除外:

  • You can add new properties to an object field.
  • 您可以使用字段映射参数来启用多字段。 field
  • 您可以更改ignore_above映射参数的值。 ignore_above

更改现有字段的映射可能会使已经建立索引的数据无效。如果需要更改字段的映射,请使用正确的映射创建一个新索引,然后将数据重新索引 reindex 到该索引中。如果只想重命名字段,请考虑添加别名 alias 字段。

查看索引的映射

您可以使用get mapping API查看现有索引的映射。

1
GET /my-index/_mapping

Query DSL

Elasticsearch提供了基于JSON的完整查询DSL(特定于域的语言)来定义查询。将查询DSL视为查询的AST(抽象语法树),它由两种子句组成:

  • Leaf query clauses

    叶子查询子句在特定字段中查找特定值,例如match,term或range查询。这些查询可以自己使用。

  • Compound query clauses

    复合查询子句包装其他叶查询或复合查询,并用于以逻辑方式组合多个查询(例如bool或dis_max查询),或更改其行为(例如constant_score查询)。

查询子句的行为会有所不同,具体取决于它们是在查询上下文中还是在过滤器上下文中使用。 query context or filter context.

查询和过滤上下文

Relevance scores

默认情况下,Elasticsearch按相关性得分对匹配的搜索结果进行排序,该得分衡量每个文档与查询的匹配程度。

相关性分数是一个正浮点数,在searchAPI的_score元字段中返回。 _score越高,文档越相关。虽然每种查询类型可以不同地计算相关性分数,但是分数计算还取决于查询子句是在查询上下文中还是在过滤器上下文中运行。

Query context 查询上下文

在查询上下文中,查询子句回答“此文档与该查询子句的匹配程度如何”的问题。除了确定文档是否匹配外,查询子句还计算_score元字段中的相关性得分。

每当将查询子句传递到查询参数(例如搜索API中的查询参数)时,查询上下文即生效。

Filter context 过滤上下文

在过滤器上下文中,查询子句回答问题“此文档是否与此查询子句匹配?”答案是简单的“是”或“否”,即不计算分数。过滤器上下文主要用于过滤结构化数据,例如

  • Does this timestamp fall into the range 2015 to 2016?
  • Is the status field set to "published"?

常用过滤器将由Elasticsearch自动缓存,以提高性能。

每当将查询子句传递到过滤器参数(例如bool查询中的filter或must_not参数,constant_score查询中的filter参数或过滤器聚合)时,过滤器上下文即生效。

Example of query and filter contexts

以下是在searchAPI的查询和过滤器上下文中使用的查询子句的示例。此查询将匹配满足以下所有条件的文档:

  • The title field contains the word search.
  • The content field contains the word elasticsearch.
  • The status field contains the exact word published.
  • The publish_date field contains a date from 1 Jan 2015 onwards.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /_search
{
"query": {
"bool": {
"must": [
{ "match": { "title": "Search" }},
{ "match": { "content": "Elasticsearch" }}
],
"filter": [
{ "term": { "status": "published" }},
{ "range": { "publish_date": { "gte": "2015-01-01" }}}
]
}
}
}
  • The query parameter indicates query context.
  • bool和两个match子句用于查询上下文,这意味着它们用于对每个文档的匹配程度进行评分。
  • filter参数指示过滤器上下文。其termrange子句用于过滤器上下文。它们将过滤出不匹配的文档,但不会影响匹配文档的分数。

在查询上下文中为查询计算的分数表示为单精度浮点数;它们只有24位才能表示有效的精度。超过有效位数的分数计算将被转换为浮点数而失去精度。

在查询上下文中使用查询子句来确定会影响匹配文档得分的条件(即文档匹配程度),并在过滤器上下文中使用所有其他查询子句。

Compound queries 复合查询

复合查询包装其他复合查询或叶查询,以组合其结果和分数,更改其行为或从查询切换到过滤器上下文。

该组中的查询是:

  • bool query

    默认查询,用于组合多个叶子或复合查询子句(must,should,must_not或filter子句)。must和should子句的分数组合在一起-匹配的子句越多越好-而must_not和filter子句在过滤器上下文中执行。

  • boosting query

    返回与肯定查询匹配的文档,但减少与否定查询匹配的文档的分数。

  • constant_score query

    一个查询,它包装另一个查询,但是在过滤器上下文中执行它。所有匹配的文档都使用相同的“常量” _score

  • dis_max query

    一个查询,它接受多个查询,并返回与任何查询子句匹配的任何文档。当布尔查询合并所有匹配查询的分数时,dis_max查询使用单个最佳匹配查询子句的分数。

  • function_score query

    使用functions修改主查询返回的分数,以考虑诸如受欢迎程度,新近度,距离或使用脚本实现的自定义算法等因素。

Boolean query

与文档匹配的查询,这些文档与其他查询的布尔组合匹配。布尔查询映射到Lucene BooleanQuery。它是使用一个或多个布尔子句构建的,每个子句都具有类型的出现。发生类型为:

Occur Description
must 子句(查询)必须出现在匹配的文档中,并将有助于得分。
filter 子句(查询)必须出现在匹配的文档中。但是与查询分数不同的是,忽略该分数。 Filter子句在过滤器上下文中执行,这意味着计分被忽略,并且子句被视为用于缓存。
should 子句(查询)应出现在匹配的文档中。
must_not 子句(查询)不得出现在匹配的文档中。子句在过滤器上下文中执行,这意味着计分被忽略,并且子句被视为用于缓存。由于计分被忽略,因此所有文档的分数均返回0。

The bool query takes a more-matches-is-better approach, so the score from each matching must or should clause will be added together to provide the final _score for each document.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "user" : "kimchy" }
},
"filter": {
"term" : { "tag" : "tech" }
},
"must_not" : {
"range" : {
"age" : { "gte" : 10, "lte" : 20 }
}
},
"should" : [
{ "term" : { "tag" : "wow" } },
{ "term" : { "tag" : "elasticsearch" } }
],
"minimum_should_match" : 1,
"boost" : 1.0
}
}
}

在filter元素下指定的查询对得分没有影响-得分将返回0。得分仅受指定查询的影响。

Boosting query

返回匹配肯定查询的文档,同时降低也匹配否定查询的文档的相关性得分。

您可以使用boosting查询来降级某些文档,而不必将它们从搜索结果中排除。

positive

(必需的查询对象)要运行的查询。返回的所有文档都必须与此查询匹配。

negative

(必需的查询对象)查询用于降低匹配文档的相关性得分。

如果返回的文档与肯定查询和该查询匹配,则增强查询将计算该文档的最终相关性得分,如下所示:

  1. 从肯定查询中获取原始的相关性分数。
  2. 将分数乘以negative_boost值。

negative_boost

(必需,浮点数)0到1.0之间的浮点数,用于降低与否定查询匹配的文档的相关性得分。

Example request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GET /bank/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"state": "ID"
}
},
"negative": {
"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}
},
"negative_boost": 0.5
}
}
}

Constant score query

包装过滤查询,并返回每个相关文档的相关性得分等于boost 参数值。

filter

(必需的查询对象)过滤要运行的查询。返回的所有文档都必须与此查询匹配。

过滤查询不计算相关性分数。为了提高性能,Elasticsearch自动缓存经常使用的过滤器查询。

boost

(可选,float)浮点数用作与查询匹配的每个文档 的恒定 相关性得分filter。默认为1.0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET /bank/_search
{
"query": {
"constant_score": {
"filter": {"range": {
"balance": {
"gte": 20000,
"lte": 30000
}
}},
"boost": 0.1
}
}
}

Full text queries 全文检索

全文查询使您可以搜索已分析的文本字段,例如电子邮件的正文。使用在索引期间应用于字段的同一分析器来处理查询字符串。

该组中的查询是:

  • intervals query

    全文查询,可以对匹配项的顺序和接近度进行细粒度控制。

  • match query

    用于执行全文查询的标准查询,包括模糊匹配和短语或接近查询。

  • match_bool_prefix query

    创建一个bool与每个词条匹配的term查询作为查询,但最后一个词条作为prefix查询匹配

  • match_phrasequery

    match查询类似,但用于匹配确切的短语或单词接近匹配。

  • match_phrase_prefix query

    match_phrase查询类似,但是对最后一个单词进行通配符搜索。

  • multi_match query

    match查询的多字段版本。

  • common terms query

    一个更专业的查询,它对不常见的单词给予更多的偏爱。

  • query_stringquery

    支持紧凑的Lucene 查询字符串语法,允许您在单个查询字符串中指定AND | OR | NOT条件和多字段搜索。仅限于专业用户。

  • simple_query_stringquery

    query_string适用于直接向用户公开的语法的更简单,更可靠的版本。

Intervals query

根据匹配项的顺序和接近程度返回文档。

间隔查询使用匹配规则,该规则由一小组定义构成。然后将这些规则应用于来自指定字段的术语。

这些定义产生的最小间隔序列跨越了文本主体中的各个术语。这些间隔可以由父源进一步组合和过滤。

Match query

返回与提供的文本,数字,日期或布尔值匹配的文档。匹配之前分析提供的文本。

匹配查询是用于执行全文搜索的标准查询,其中包括模糊匹配的选项。

1
2
3
4
5
6
7
8
9
10
GET /_search
{
"query": {
"match" : {
"message" : {
"query" : "this is a test"
}
}
}
}

REST APIs

Index APIs

Create Index

创建一个新索引。

1
PUT /twitter

Request

1
PUT /<index>

Description

您可以使用create index API将新索引添加到Elasticsearch集群。创建索引时,可以指定以下内容:

  • Settings 索引设置
  • Mappings 索引中字段的映射
  • Index aliases 索引别名

Path parameters

**<index>**(可选,字符串)您要创建的索引的名称。

索引名称必须满足以下条件:

  • Lowercase only
  • Cannot include \, /, *, ?, ", <, >, |, (space character), ,, #
  • Indices prior to 7.0 could contain a colon (:), but that’s been deprecated and won’t be supported in 7.0+
  • Cannot start with -, _, +
  • Cannot be . or ..
  • Cannot be longer than 255 bytes (note it is bytes, so multi-byte characters will count towards the 255 limit faster)

Query parameters

  • include_type_name

    [7.0.0] Deprecated in 7.0.0. Mapping types have been deprecated. See Removal of mapping types.(Optional, boolean) If true, a mapping type is expected in the body of mappings. Defaults to false.

  • wait_for_active_shards

    (可选,字符串)在继续操作之前必须处于活动状态的分片副本数。设置为全部或任何正整数,直到索引中的分片总数(number_of_replicas + 1)。默认值:1,主分片。

  • timeout

    (可选,时间单位)指定等待响应的时间。如果在超时到期之前未收到任何响应,则请求将失败并返回错误。默认为30秒。

  • master_timeout

    (可选,时间单位)指定等待连接到主节点的时间段。如果在超时到期之前未收到任何响应,则请求将失败并返回错误。默认为30秒。

Request body

Examples例子

Index settings索引设置

创建的每个索引都可以具有与之关联的特定设置,这些设置在主体中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PUT /twitter
{
"settings" : {
"index" : {
"number_of_shards" : 3, 默认1
"number_of_replicas" : 2 默认1
}
}
}

PUT /twitter
{
"settings" : {
"number_of_shards" : 3,
"number_of_replicas" : 2
}
}
Mapping

创建索引API允许提供映射定义:

1
2
3
4
5
6
7
8
9
10
11
PUT /test
{
"settings" : {
"number_of_shards" : 1
},
"mappings" : {
"properties" : {
"field1" : { "type" : "text" }
}
}
}
Aliases
1
2
3
4
5
6
7
8
9
10
11
12
PUT /test
{
"aliases" : {
"alias_1" : {},
"alias_2" : {
"filter" : {
"term" : {"user" : "kimchy" }
},
"routing" : "kimchy"
}
}
}

默认情况下,索引创建仅在每个分片的主副本已启动或请求超时时才向客户端返回响应。索引创建响应将指示发生了什么:

1
2
3
4
5
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "test"
}

acknowledged 指示索引是否在集群中成功创建, 而shards_acknowledged 指示在超时之前是否为索引中的每个分片启动了必要数量的分片副本。请注意,acknowledgedshards_acknowledged仍然可能为假,但索引创建成功。 这些值仅表示操作是否在超时之前完成。如果accepted为false,那么在使用新创建的索引更新集群状态之前,我们会超时,但是可能很快就会创建它。 If shards_acknowledged is false, then we timed out before the requisite number of shards were started (by default just the primaries), even if the cluster state was successfully updated to reflect the newly created index (i.e. acknowledged=true).

我们可以通过索引设置index.write.wait_for_active_shards更改仅等待主分片启动的默认设置

Search APIs

Most search APIs are multi-index, with the exception of the Explain API endpoints.

返回与请求中定义的查询匹配的搜索命中。

1
GET /twitter/_search?q=tag:wow

Request

GET /<index>/_search

POST /<index>/_search

GET /_search

POST /_search

Description

允许您执行搜索查询并获取与查询匹配的搜索命中。可以使用简单查询字符串作为参数来提供查询,也可以使用请求正文来提供查询。 query string as a parameter, or using a request body.

为确保快速响应,如果一个或多个分片失败,搜索API将以部分结果响应。有关更多信息,请参见分片故障。

Path parameters

**<index>**(可选,字符串)索引名称的逗号分隔列表或通配符表达式,用于限制请求。

Query parameters

allow_no_indices

(可选,布尔值)如果为true,则如果通配符表达式或_all值仅检索丢失或闭合的索引,则请求不会返回错误。

此参数还适用于指向别名缺失或封闭索引的索引别名。

allow_partial_search_results

(可选,布尔值)指示如果部分搜索失败或超时,是否应返回错误。默认为true。

Request body

query

(Optional, query object) Defines the search definition using the Query DSL.