Micro Major-Java Senior Architect

线程通信

JDK API

suspend / resume

调用 suspend 挂起目标线程,通过 resume 恢复线程。已被废弃!被弃用的主要原因是,容易写出死锁的代码。所以用 wait/notify 和 park/unpark 机制进行替代。

死锁示例 1:在同步代码中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 死锁的suspend/resume。 suspend并不会像wait一样释放锁,故此容易写出死锁代码 */
public void suspendResumeDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
Thread.currentThread().suspend();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
consumerThread.resume();
}
System.out.println("3、通知消费者");
}
死锁示例 2:suspend 在 resume 后执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/** 导致程序永久挂起的suspend/resume */
public void suspendResumeDeadLockTest2() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) {
System.out.println("1、没包子,进入等待");
try { // 为这个线程加上一点延时
Thread.sleep(5000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 这里的挂起执行在resume后面
Thread.currentThread().suspend();
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
consumerThread.resume();
System.out.println("3、通知消费者");
consumerThread.join();
}

wait / notify

wait 和 notify 方法只能由同一对象锁的持有者线程调用,也就是写在同步代码块里面,否则会抛出 aaa 异常。

  • wait 方法导致当前线程等待,加入该对象的等待集合中,并且释放当前持有的对象锁。
  • notify/notifyAll 方法唤醒一个或所有正在等待这个对象锁的线程。

注意:虽然 wait 会自动解锁,但是对顺序还是有要求,如果在 notify 被调用之后,才开始调用 wait,线程会永远处于 WAITING 状态。

永久等待示例
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
/** 会导致程序永久等待的wait/notify */
public void waitNotifyDeadLockTest() throws Exception {
// 启动线程
new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
try {
Thread.sleep(5000L);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
synchronized (this) {
try {
System.out.println("1、进入等待");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println("2、买到包子,回家");
}).start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
synchronized (this) {
this.notifyAll();
System.out.println("3、通知消费者");
}
}

park / unpark

线程调用 park 则等待“许可”,调用 unpark 方法为指定线程提供“许可(permit)”。不要求 park 和 unpack 方法的调用顺序

多次调用 unpack 后,再调用 park,线程会直接运行。但不会叠加,也就是说,连续多次调用 park 方法,第一次会拿到“许可”直接运行,后续调用会进入等待。“许可”类似于标志位。

死锁示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/** 死锁的park/unpark */
public void parkUnparkDeadLockTest() throws Exception {
// 启动线程
Thread consumerThread = new Thread(() -> {
if (baozidian == null) { // 如果没包子,则进入等待
System.out.println("1、进入等待");
// 当前线程拿到锁,然后挂起
synchronized (this) {
LockSupport.park();
}
}
System.out.println("2、买到包子,回家");
});
consumerThread.start();
// 3秒之后,生产一个包子
Thread.sleep(3000L);
baozidian = new Object();
// 争取到锁以后,再恢复consumerThread
synchronized (this) {
LockSupport.unpark(consumerThread);
}
System.out.println("3、通知消费者");
}

伪唤醒

警告!之前代码中使用 if 语句来判断是否进入等待状态是错误的!

官方建议应该在循环中检查等待条件,原因是处于等待状态的线程可能会收到错误警报和伪唤醒,如果不在循环中检查等待条件,程序就会在没有满足结束条件的情况下退出。

伪唤醒是指线程并非因为 notify、notifyAll、unpark 等 API 调用而唤醒,而是因为更底层原因导致的。

总结

同步代码 代码顺序
suspend/resume 死锁 永久挂起
wait/notify 永久等待
park/unpack 死锁

使用 while 循环代替 if 条件判断是否进入等待状态来避免伪唤醒。

线程封闭

通过将数据封闭在线程中而避免使用同步的技术称为线程封闭

Java 中具体体现为 ThreadLocal、局部变量等。

ThreadLocal

ThreadLocal 是 Java 里一种特殊的变量。

ThreadLocal 是线程级别变量,每个线程都有自己独立的一个变量,竞争条件被彻底消除了,在并发模式下是绝对安全的变量。

可以理解为,JVM 维护了一个 Map<Thread, T>,每个线程要用这个 T 的时候,用当前的线程去 Map 里面取。

线程封闭示例
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
/** 线程封闭示例 */
public class Demo7 {
/** threadLocal变量,每个线程都有一个副本,互不干扰 */
public static ThreadLocal<String> value = new ThreadLocal<>();

/**
* threadlocal测试
*
* @throws Exception
*/
public void threadLocalTest() throws Exception {

// threadlocal线程封闭示例
value.set("这是主线程设置的123"); // 主线程设置值
String v = value.get();
System.out.println("线程1执行之前,主线程取到的值:" + v);

new Thread(new Runnable() {
@Override
public void run() {
String v = value.get();
System.out.println("线程1取到的值:" + v);
// 设置 threadLocal
value.set("这是线程1设置的456");

v = value.get();
System.out.println("重新设置之后,线程1取到的值:" + v);
System.out.println("线程1执行结束");
}
}).start();

Thread.sleep(5000L); // 等待所有线程执行结束

v = value.get();
System.out.println("线程1执行之后,主线程取到的值:" + v);

}

public static void main(String[] args) throws Exception {
new Demo7().threadLocalTest();
}
}

栈封闭

局部变量的固有属性之一就是封闭在线程中。

线程池

线程是不是越多越好?为什么要用线程池?

  1. 线程在 Java 中是一个对象,需要占用内存,更需要占用操作系统的资源,线程的创建、销毁都需要时间。如果创建时间 + 销毁时间 > 执行任务时间,就很不划算。
  2. Java 对象占用堆内存,操作系统线程占用系统内存,根据 JVM 规范,一个线程默认最大栈大小为 1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗大量的内存。
  3. 线程过多时,操作系统需要频繁切换线程上下文,影响性能。

线程池的推出,就是为了方便的控制线程数量。

概念

  1. 线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
  2. 工作线程:线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务;
  3. 任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列:用于存放没有处理的任务,提供一种缓冲机制。

API

接口定义和实现类

类型 名称 描述
接口 Executor 最上层的接口,定义了执行任务的方法 execute
接口 ExecutorService 继承了 Executor 接口,拓展了 Callable、Future、关闭方法
接口 ScheduledExecutorService 继承了 ExecutorService,增加了定时任务相关的方法
实现类 ThreadPoolExecutor 基础、标准的线程池实现
实现类 ShceduledThreadPoolExecutor 继承了 ThreadPoolExecutor,实现了 ScheduledExecutorService 中定时任务相关的方法
ExecutorService
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
public interface ExecutorService extends Executor {

// 监测 ExecutorService 是否已经关闭,直到所有任务执行完成,或发生超时,或当前线程被中断
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

// 执行给定的任务集合,执行完毕后,返回结果
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

// 执行给定的任务集合,执行完毕或者超时后,返回结果,其他任务终止
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

// 执行给定的任务,任意一个执行成功或者超时后,则返回结果,其他任务终止
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

// 如果此线程池已关闭,则返回 true
boolean isShutdown();

// 如果关闭后所有任务都已完成,则返回 true。
boolean isTerminated();

// 优雅关闭线程池,之前提交的任务将被执行,但是不会接受新的任务。
void shutdown();

// 尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表。
List<Runnable> shutdownNow();

// 提交一个用于执行的 Callable 返回任务,并返回一个 Future,用于获取 Callable 执行结果。
<T> Future<T> submit(Callable<T> task);

// 提交可运行任务以执行,并返回一个 Future 对象,执行结果为 null
Future<?> submit(Runnable task);

// 提交可运行任务以执行,并返回一个 Future 对象,执行结果为传入的 result
<T> Future<T> submit(Runnable task, T result);
}
ScheduledExecutorService

创建并执行一个一次性任务,过了延迟时间就会被执行

1
2
3
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

创建并执行一个周期性任务,过了给定的初始延迟时间,会第一次被执行,执行过程中发生了异常,那么任务就停止。

一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束后立即执行,这也是它和 scheduleWithFixedDelay 的重要区别。

1
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);

创建并执行一个周期性任务,过了给定的初始延迟时间,第一次被执行后,后续以给定的周期时间执行,执行过程中发生了异常,那么任务就停止。

一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束的时间基础上,计算执行延时

Executors 工具类

  • public static ExecutorService newFixedThreadPool(int nThreads)

    创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数。

  • public static ExecutorService newCachedThreadPool()

    创建一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行。池中的线程空闲超过 60 秒,将被销毁释放。线程数随任务的多少变化。适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数=Integer.MAX_VALUE

  • public static ScheduledExecutorService newSingleThreadScheduledExecutor()

    只有一个线程来执行无界任务队列的单一线程池。该线程池确保任务按加入的顺序一个一个一次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续的任务。与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能再改变。

  • public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

    能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数=Interger.MAX_VALUE

任务执行过程

ThreadPoolExecutor.execute()

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
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
            graph TD
            
A[执行] --> B{是否达到核心线程数}

B --> |否|D[创建新线程执行]

B --> |是|C{工作队列是否已满}

C --> |否|E[丢到队列里]

C --> |是|F{是否达到最大线程数量}

F --> |否|G[新建线程执行]

F --> |是|H[执行拒绝策略]

          

线程数量

如何确定合适数量的线程?

  • 计算型任务:CPU 数量的 1-2 倍
  • IO型任务:相对计算型任务,需要多一些线程,要根据具体的 IO 阻塞时长进行考量决定。如 tomcat 中默认的最大线程数为 200。
  • 也可以考虑根据需要在一个最小数量和最大数量间自动增减线程数。

CAS 机制

CAS(Compare and swap 比较和交换)属于硬件同步原语,处理器提供了基本内存操作的原子性保证。

CAS 操作需要输入两个数值,一个旧值A和一个新值B,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换新值,否则不交换。

Java 中的 sun.misc.Unsafe 类,提供了 compareAndSwapInt() 和 compareAndSwapIong() 等几个方法实现 CAS。

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
package com.gzhennaxia.demo;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class Demo {

private volatile int value;
private static Unsafe unsafe;
private static long offset;

static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
offset = unsafe.objectFieldOffset(Demo.class.getDeclaredField("value"));
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}

public void inc() {
value++;
}

public void incCAS() {
int current;
do {
current = unsafe.getIntVolatile(this, offset);
} while (!unsafe.compareAndSwapInt(this, offset, current, current + 1));
}

public static void main(String[] args) throws InterruptedException {
Demo demo10 = new Demo();
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// demo10.inc();
demo10.incCAS();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(demo10.value);
}
}

J.U.C 包内的原子操作封装类

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型
  • AtomicIntegerArray:原子更新整型数组里的元素
  • AtomicLongArray:原子更新长整形数组里的元素
  • AtomicReferenceArray:原子更新引用类型数组里的元素
  • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
  • AtomicLongFieldUpdater:原子更新长整形字段的更新器
  • AtomicReferenceFieldUpdater:原子更新饮用类型的字段
  • AtomicReference:原子更新引用类型
  • AtomicStampedReference:原子更新带有版本号的引用类型
  • AtomicMarkableReference:原子更新带有标记位的引用类型

1.8 更新:

更新器:DoubleAccumulator、LongAccumulator

计数器:DoubleAdder、LongAdder

计数器增强版,高并发下性能更好

频繁更新但不太频繁读取的汇总统计信息时使用

分成多个操作单元,不同线程更新不同的单元

只有需要汇总的时候才计算所有单元的操作

CAS 的三个问题

  1. 循环+CAS,自旋的实现让所有线程都处于高频运行,争抢 CPU 执行时间的状态。如果操作长时间不成功,会带来很大的 CPU 资源消耗。

  2. 仅针对单个变量的操作,不能用于多个变量来实现原子操作。

  3. ABA 问题

    线程1将A修改为B,线程2将B修改为C,线程3将B修改为A。

    如果执行顺序为线程1 -> 线程3 -> 线程2,则线程2将无法感知到线程1和线程3的修改。

Java 锁

自旋锁:为了不放弃 CPU 执行事件,循环的使用 CAS 技术对数据进行更新,直至成功。

悲观锁:假定会发生并发冲突,同步所有对数据的相关操作,从读数据就开始上锁。

乐观锁:假定没有冲突,在修改数据时如果发现和之前获取的不一样,则读最新数据,修改后重试修改。

自旋锁就是乐观锁的一种体现。

独享锁(写):给资源加上写锁,线程可以修改资源,其他线程不能再加锁;(单写)

共享锁(读):给资源加上读锁后只能读不能改,其他线程也只能加读锁,不能加写锁;(多读)

可重入锁、不可重入锁:线程拿到一把锁之后,可以自由进入同一把锁所同步的其他代码。

公平锁、非公平锁:争抢锁的顺序,如果是按先来后到,则为公平。

几种重要的锁实现:synchronized、ReentrantLock、ReentrantReadWriteLock

同步关键字 synchronized

synchronized 属于最基本的线程通信机制,基于对象监视器实现。

Java 中的每个对象都与一个监视器相关联,一个线程可以锁定或解锁。

一次只有一个线程可以锁定监视器,试图锁定该监视器的任何其他线程都会被阻塞,直到它们可以获得该监视器上的锁定为止。

特性:可重入、独享、悲观锁

锁的范围:类锁、对象锁、锁消除、锁粗化

提示:同步关键字,不仅是实现同步,根据 JMM 规定,还能保证可见性(读取最新主内存数据,结束后写入主内存)。

示例

可重入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可重入
public class ObjectSyncDemo2 {

public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我开始执行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我执行结束" + arg);
}

public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
锁粗化
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
// 锁粗化(运行时 jit 编译优化)
// jit 编译后的汇编内容, jitwatch可视化工具进行查看
// just-in-time compilation,缩写为JIT;又译及时编译、实时编译
public class ObjectSyncDemo3 {
int i;

public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
}

// 优化后只锁定一次
// public void test1(Object arg) {
// synchronized (this) {
// i++;
// i++;
// }
// }

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
// 热点代码会进行分析后优化
new ObjectSyncDemo3().test1("a");
}
}
}
锁消除
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
// 锁消除(jit)
public class ObjectSyncDemo4 {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}

public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}


public void test1(Object arg) {
// jit 优化, 消除了锁
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}

public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo4().test1("123");
}
}
}

synchronized 加锁原理

死磕Synchronized底层实现–概论 - 掘金

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。当条件不满足时,锁会按偏向锁->轻量级锁->重量级锁的顺序升级。

The Hotspot Java Virtual Machine

https://wiki.openjdk.java.net/display/hotspot/synchronization

Lock 锁接口实现

Lock 核心API

API 描述
lock 获取锁的方法,若锁被其他线程获取,则等待(阻塞)
lockInterruptibly 在锁的获取过程中可以中断当前线程
tryLock 尝试非阻塞地获取锁,立即返回
unlock 释放锁

提示:根据Lock接口的源码注释,Lock接口的实现,具备和同步关键字同样的内存语义。

ReentrantLock

  • 独享锁
  • 支持公平、非公平两种模式
  • 可重入
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
// 可响应中断
public class LockInterruptiblyDemo1 {
private Lock lock = new ReentrantLock();

public static void main(String[] args) throws InterruptedException {
LockInterruptiblyDemo1 demo1 = new LockInterruptiblyDemo1();
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
demo1.test(Thread.currentThread());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
Thread.sleep(500); // 等待0.5秒,让thread1先执行

thread2.start();
Thread.sleep(2000); // 两秒后,中断thread2

thread2.interrupt();
}

public void test(Thread thread) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + ", 想获取锁");
lock.lockInterruptibly(); //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
try {
System.out.println(thread.getName() + "得到了锁");
Thread.sleep(10000); // 抢到锁,10秒不释放
} finally {
System.out.println(Thread.currentThread().getName() + "执行finally");
lock.unlock();
System.out.println(thread.getName() + "释放了锁");
}
}
}

ReadWriteLock

维护一对关联锁,一个用于只读操作,一个用于写入;读锁可以由多个读线程同时持有,写锁是排他的。

适合读取线程比写入线程多的场景,改进互斥锁的性能,示例场景:缓存组件、集合的并发线程安全性改造。

锁降级指的是写锁降级为读锁。把持住当前拥有的写锁的同时,再获取到读锁,随后释放写锁的过程。

写锁是线程独占,读锁是共享,所以写->读是降级。(无法实现读->写)

Codition

用于替代 wait/notify

Object 中的 wait(),notify(),notifyAll() 方法是和 synchronized 配合使用的,可以唤醒一个或者全部(单个等待集);

Condition 是需要与 Lock 配合使用的,提供了多个等待集合,更精确的控制(底层是 park/unpark 机制);

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
// condition 实现队列线程安全。
public class QueueDemo {
final Lock lock = new ReentrantLock();
// 指定条件的等待 - 等待有空位
final Condition notFull = lock.newCondition();
// 指定条件的等待 - 等待不为空
final Condition notEmpty = lock.newCondition();

// 定义数组存储数据
final Object[] items = new Object[100];
int putptr, takeptr, count;

// 写入数据的线程,写入进来
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) // 数据写满了
notFull.await(); // 写入数据的线程,进入阻塞
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 唤醒指定的读取线程
} finally {
lock.unlock();
}
}
// 读取数据的线程,调用take
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 线程阻塞在这里,等待被唤醒
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 通知写入数据的线程,告诉他们取走了数据,继续写入
return x;
} finally {
lock.unlock();
}
}
}

AQS 抽象队列同步器

运用到模版方法设计模式

即算法的整体骨架已确定,某些具体的步骤由子类去实现。

提供了对资源占用、释放,线程的等待、唤醒等等接口和具体实现。

可以用在各种需要控制资源争用的场景中。(ReentrantLock/CountDownLatch/Semphore)

image-20201013163241377

acquire、acquireShared:定义了资源争用的逻辑,如果没拿到,则等待。

tryAcquire、tryAcquireShared:实际执行占用资源的操作,如何判定由使用者具体去实现。

release、releaseShared:定义释放资源的逻辑,释放之后,通知后续节点进行争抢。

tryRelease、tryReleaseShared:实际执行资源释放的操作,具体的AQS使用者去实现。

            graph TD
            
A[acquire] --> B{tryAcquire}

B --> D[加入队尾]

B --> |抢到资源|C[end]

D --> E[寻找前置]

E --> F{前置节点是否为头节点}

F --> |是|G{tryAcquire}

F --> |否 park|H[等待]

H --> |unpark/interrupt|F

G --> |抢到资源|C

G --> |没抢到资源|E

          

信号量、栅栏和倒计时器

Semaphore

Semaphore 称为“信号量”,控制多个线程争抢许可。

  • acquire:获取一个许可,如果没有就等待。
  • release:释放一个许可。
  • availablePermits:获取可用许可数目

典型场景:

  • 代码并发处理限流(Hatrix);

CountDownLatch

Java1.5 被引入的一个工具类,常被称为“倒计数器”。

创建对象时,传入指定数值作为线程参与的数量;

await:该方法等待计数器变为0,在这之前,线程进入等待状态;

countdown:计数器数值减一,知道为0;

经常用于等待其他线程执行到某一节点,再继续执行当前线程代码。

使用场景示例:

  • 统计线程执行的情况;
  • 压力测试中,使用 countDownLatch 实现最大程度的并发处理;
  • 多个线程之间,相互通信,比如线程异步调用接口,结果通知;
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
public class CountDownLatchDemo {

public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + finalI + "执行结束");
countDownLatch.countDown();
}
}).start();
}
try {
countDownLatch.await();
System.out.println("主线程等待10个线程执行完后再往下执行");
} catch (InterruptedException e) {
e.printStackTrace();
}

}
}

CyclicBarrier

CyclicBarrier 也是1.5引入的,又称为“线程栅栏”。

创建对象时,指定栅栏线程数量。

await:等指定数量多线程都处于等待状态时,继续执行后续代码。

barrierAction:线程数量到了指定量后,自动触发执行指定任务。

和 CountDownLatch 的重要区别在于,CyclicBarrier 对象可以多次触发执行。

典型场景:

  • 数据量比较大时,实现批量插入数据到数据库;
  • 数据统计,30个线程统计30天数据,全部统计完毕后,执行汇总;

J.U.C 并发容器类

Map

HashMap / ConcurrentHashMap 源码分析

探索 ConcurrentHashMap 高并发性的实现机制

HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!

Java进阶(六)从ConcurrentHashMap的演进看Java多线程核心技术

ConcurrentSkipListMap

J.U.C 之ConcurrentSkipListMap - 掘金

Skip list | 维基百科

特点:

  • 有序链表实现

  • 无锁实现(使用CAS)

  • value 不能为空

  • 层级越高,跳跃性越大,数据约少,速度越快

  • 随机决定新节点是否抽出来作为索引

  • 时间复杂度为 O(log n),空间复杂度为 O(n)

List

CopyOnWriteArrayList

CopyOnWriteArrayList 容器即写时复制容器,和 ArrayList 相比,优点是并发安全,缺点有两个:

  1. 多了内存占用:写数据是 copy 一份完整的数据,单独进行操作。
  2. 数据一致性:数据写完之后,其他线程不一定能马上读取到最新内容。

Set

实现 原理 特点
HashSet 基于 HashMap 实现 非线程安全
CopyOnWriteArraySet 基于 CopyOnWriteArrayList 线程安全
ConcurrentSkipListSet 基于 ConcurrentSkipListMap 线程安全,有序,查询快

Queue

方法 作用 描述
add 添加元素 如果队列已满,则抛出 IllegalStateException 异常
remove 移除并返回队列头部的元素 如果队列为空,则抛出 NoSuchElementException 异常
element 返回队列头部的元素 如果队列为空,则抛出 NoSuchElementException 异常
offer 添加一个元素并返回 true 如果队列已满,则返回 false
poll 移除并返回队列头部的元素 如果队列为空,则返回 null
peek 返回队列头部的元素 如果队列为空,则返回 null
put 添加一个元素 如果队列已满,则阻塞
take 移除并返回队列头部的元素 如果队列为空,则阻塞

fork/join 并发处理框架

使用ForkJoin - 廖雪峰的官方网站

Java的Fork/Join任务,你写对了吗? - 廖雪峰的官方网站

fork join | 并发编程网– ifeve.com

面试大厂必问的ForkJoin框架剖析【建议收藏】

Fork/Join 框架是 Java7 提供了的一个用于并行执行任务的框架, 是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

ForkJoinPool 是 ExecutorService 接口的实现,专为可以递归分解的工作而设计。

分解任务的代码一般使用 RecursiveTask(带返回结果) 或者其子类 RecursiveAction。

适合数据处理、结果汇总、统计等场景;

Java 8 实例:java.util.Arrays#parallelSort() 方法。

工作窃取带来的性能提升偏理论,API 的复杂性较高,实际研发中可控性来说不如其他 API。

Future

Future 表示异步计算的结果,提供了用于检查计算是否完成、等待计算完成以及获取结果的方法。

网络

TCP/UDP 协议

传输控制协议 TCP

https://github.com/jawil/blog/issues/14

https://juejin.im/post/6844903958624878606

传输控制协议(TCP,Transmission Control Protocol)是一个传输层协议,提供面向连接的、可靠的、有序的、基于字节流的传输层通信协议。应用程序在使用 TCP 之前,必须先建立 TCP 连接。

TCP 三次握手
TCP 四次挥手
为什么需要四次挥手?

四次挥手是为了连接双方能够安全的释放资源(连接双方会存储对方的地址信息)。

少于四次无法实现这个目的,假如客户端率先发起断开连接的请求:

  1. 第一次挥手:客户端发送断开连接的请求给服务器

    此时服务器由于还需要发报文给客户端,因此无法释放资源(即删除客户端地址信息);而客户端还没有收到服务器的确认报文,需要持续监听,因此也无法释放资源(即删除服务器地址信息)。

  2. 第二次挥手:服务器发送确认报文给客户端

    此时服务器由于还需要发送数据报文给客户端,因此无法自身释放资源;而客户端还需要继续监听服务端,也无法释放自身资源。

  3. 第三次挥手:服务器发送断开连接的请求给客户端

    此时服务器由于还没收到客户端的确认报文,需要继续监听,所以无法释放资源;而客户端需要发送确认报文,也无法释放资源。

  4. 第四次挥手:客户端发送确认报文给服务器

    当服务器收到该报文后就可以释放资源了,但客户端由于无法确定该报文是否被收到了,所以还无法释放资源,需要等待 2MSL 时间后才能释放资源。

四次挥手是为了连接双方能够断开彼此之间的数据通道。少于四次无法实现这个目的。

假如客户端率先发起断开连接的请求:

  1. 第一次挥手:客户端发送断开连接的请求给服务器

    为了实现安全性,需要等待服务器的确认报文。

  2. 第二次挥手:服务器发送确认报文给客户端

    由于 TCP 是全双工模式,双通道相互独立,故服务器还可以继续发送数据报文给客户端,此时客户端往服务器端的数据通道关闭,TCP 连接处于半关闭状态。

  3. 第三次挥手:服务器发送断开连接的请求给客户端

    为了安全性,需要等待客户端的确认报文。

  4. 第四次挥手:客户端发送确认报文给服务器

    当服务器接收到该报文后即可关闭连接,但客户端在发送完该报文后并不能确保服务器是否已收到,需要等待 2MSL 时间后再关闭。

2MSL等待状态

报文段最大生存时间MSL(Maximum Segment Lifetime),在第四次挥手后主动端之所以还需要等待 2MSL 时间,是因为它无法保证报文是否被接收到,但如果假设报文丢失了,那么被动端会在 2MSL 时间内再发送一次断开连接的请求,此时主动端就可以判定确认报文丢失了,然后重新发送一次确认报文,如果 2MSL 时间内没有收到被动端再一次的断开连接请求,就认为被动端已经收到确认报文了,就可以关闭连接了。

用户数据报协议 UDP

用户数据报协议属于传输层协议。提供无连接、不可靠、数据报尽力传输服务。

Socket 编程

应用最广泛的网络应用编程接口

  • 数据报类型套接字 SOCK_DGRAM(面向 UDP 接口)
  • 流式套接字 SOCK_STREAM(面向 TCP 接口)
  • 原始套接字 SOCK_RAW(面向网络层协议接口 IP、ICMP等)

Socket API 调用过程

            graph TD
            
A[创建套接字] --> B[端点绑定]

B --> C[发送数据]

C --> D[接收数据]

D --> F[释放套接字]

          

Socket API 函数

listen()、accept() 函数只能用于服务器端;

connect() 函数只能用于客户端;

socket()、bind()、send()、recv()、sendto()、recvfrom()、close()

BIO

阻塞 IO 的含义

同步和异步、阻塞和非阻塞

  • 阻塞(blocking)IO:资源不可用时,IO 请求一直阻塞,直到反馈结果(有数据或超时)。

  • 非阻塞(non-blocking)IO:资源不可用时,IO 请求立即返回,返回数据标识资源不可用。

  • 同步(synchronous)IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败。

  • 异步(asynchronous)IO:应用发送或接收数据后立刻返回,实际处理是异步执行的。

阻塞和非阻塞是获取资源的方式,同步/异步是程序如何处理资源的逻辑设计。

我的理解:阻塞/非阻塞是指单个线程的状态,同步/异步是指多个线程之间的关系。

API:ServerSocket#accept、InputStream#read 都是阻塞的API。操作系统底层 API 中,默认 Socket 操作都是 Blocking 型,send/recv 等接口都是阻塞的。

BIO 带来的问题:阻塞导致在处理网络 IO 时,一个线程只能处理一个网络连接。

NIO

NIO 始于 Java 1.4。

NIO 有三个核心组件:

  1. Buffer 缓冲区
  2. Channel 通道
  3. Selector 选择器

Buffer

使用 Buffer 进行数据读写,需要如下四个步骤:

  1. 将数据写入缓冲区
  2. 调用 buffer.flip() 转换为读取模式
  3. 缓冲区读取数据
  4. 调用 buffer.clear() 或 buffer.compact() 清除缓冲区
Buffer 原理

三个重要属性:

  1. capacity
  2. position:写入模式时代表写数据的位置,读取模式时代表读取数据的位置。
  3. limit:写入模式时等于 capacity,读取模式时代表写入的数据量。
ByteBuffer

ByteBuffer 为性能关键型代码提供了直接内存(direct 堆外)和非直接内存(heap 堆)两种实现。

堆外内存获取的方式:ByteBuffer.allocateDirect(noBytes);

好处:

  1. 进行网络 IO 或者文件 IO 时比 heapBuffer 少一次拷贝(file/socket — OS memory — jvm heap)。GC 会移动对象内存,在写 file 或 socket 的过程中,JVM 的实现中,会先把数据复制到堆外,再进行写入。
  2. GC 范围之外,降低 GC 压力,但实现了自动管理。DirectByteBuffer 中有一个 Cleaner 对象(PhantomReference),Cleaner 被 GC 前会执行 clean 方法,触发 DirectByteBuffer 中定义的 Deallocator

建议:

  1. 性能确实可观的时候才去使用;分配给大型、长寿命;(网络传输、文件读写场景)
  2. 通过虚拟机参数 MaxDirectMemorySize 限制大小,防止耗尽整个机器的内存;

Channel

SocketChannel

SocketChannel 用于建立 TCP 网络连接,类似 java.net.Socket。有两种创建 SocketChannel 的形式:

  1. 客户端主动发起和服务器的连接
  2. 服务端获取的新连接

write:write() 在尚未写入任何内容时就可能返回了。需要在循环中调用 write()。

read:read() 方法可能直接返回而不读取任何数据,根据返回的 int 值判断读了多少字节。

ServerSocketChannel

ServerSocketChannel 可以监听新建的 TCP 连接通道,类似 ServerSocket。

ServerSocketChannel.accept():如果该通道出去非阻塞模式,那么如果没有挂起的连接,该方法将立即返回 null。必须检查返回的 SocketChannel 是否为 null。

Selector

Selector 实现一个线程处理多个通道:事件驱动机制。

非阻塞的网络通道下,开发者通过 Selector 注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)

NIO VS BIO

BIO,阻塞 IO,线程等待时间长,一个线程负责一个连接处理,线程多且利用率低。

NIO,非阻塞 IO,线程利用率更高,一个线程处理多个连接事件,性能更强大。

Tomcat 8 中,已经完全去除 BIO 相关的网络处理代码,默认采用 NIO 进行网络处理。

NIO 与多线程结合的改进方案

Doug Lea 的著名文章《Scalable IO in Java》

小结

NIO 为开发者提供了功能丰富及强大的 IO 处理 API,但是在应用与网络应用开发的过程中,直接使用 JDK 提供的 API,比较繁琐。而且要想将性能进行提升,光有 NIO 还不够,还需要将多线程技术与之结合起来。

因为网络编程本身的复杂性,以及 JDK API 开发的使用难度较高,所以在开源社区中,涌现出很多对 JDK NIO 进行封装、增强后对网络编程框架,例如:Netty、Mina 等。

Netty

Netty 是一个高性能、高可扩展性的异步事件驱动的网络应用程序框架,它极大地简化了 TCP 和 UDP 客户端和服务器开发等网络编程。

Netty 重要的四个内容:

  1. Reactor 线程模型:一种高性能的多线程程序设计思路
  2. Netty 中自己定义的 Channel 概念:增强版的通道概念
  3. ChannelPipeline 职责链设计模式:事件处理机制
  4. 内存管理:增强的 ByteBuf 缓冲区

Netty 线程模型

为了让 NIO 处理更好的利用多线程特性,Netty 实现了 Reactor 线程模型。

Reactor 模型中有四个核心概念:

  1. Resources 资源(请求/任务)
  2. Synchronous Event Demultiplexer 同步事件复用器
  3. Dispatcher 分配器
  4. Request Handler 请求处理器

Channel 概念

Netty 中的 Channel 是一个抽象的概念,可以理解为对 JDK NIO Channel 的增强和拓展。增加了很多属性和方法。

责任链设计模式

责任链- 廖雪峰的官方网站

责任链设计模式(职责链模式) - 重构和设计模式

设计模式| 责任链模式及典型应用 - 掘金

模式结构

责任链模式包含如下角色:

  • 处理器抽象类
  • 具体处理器
  • 处理器链维护器(可选):维护了各个处理器的前后关系。可以由客户端再发送请求前生成链,或者动态地生成链。
  • 客户端
伪例

请假流程

请假请求类:LeaveRequest

1
2
3
4
5
6
7
8
public class LeaveRequest {

int days;

public LeaveRequest(int days) {
this.days = days;
}
}

处理器抽象类:LeaveRequestHandler

1
2
3
4
5
6
7
8
9
public abstract class LeaveRequestHandler {
int threshold;

public LeaveRequestHandler(int threshold) {
this.threshold = threshold;
}

public abstract Boolean handle(LeaveRequest leaveRequest);
}

处理器具体类:SupervisorHandler、ManagerHandler、GeneralManagerHandler

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
public class SupervisorHandler extends LeaveRequestHandler {
public SupervisorHandler(int threshold) {
super(threshold);
}

@Override
public Boolean handle(LeaveRequest leaveRequest) {
if (leaveRequest.days <= threshold){
return new Random().nextBoolean();
}
return null;
}
}

public class ManagerHandler extends LeaveRequestHandler {
public ManagerHandler(int threshold) {
super(threshold);
}

@Override
public Boolean handle(LeaveRequest leaveRequest) {
if (leaveRequest.days <= threshold){
return new Random().nextBoolean();
}
return null;
}
}

public class GeneralManagerHandler extends LeaveRequestHandler {

public GeneralManagerHandler(int threshold) {
super(threshold);
}

@Override
public Boolean handle(LeaveRequest leaveRequest) {
if (leaveRequest.days <= threshold){
return new Random().nextBoolean();
}
return null;
}
}

客户端:Worker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Worker {
public static void main(String[] args) {
int days = 3;
Boolean response = requestLeave(days);
System.out.println("申请" + days + "天," + (response ? "申请成功!" : "申请失败!"));
}

private static Boolean requestLeave(int days) {
LeaveRequest leaveRequest = new LeaveRequest(days);
SupervisorHandler supervisorHandler = new SupervisorHandler(3);
ManagerHandler managerHandler = new ManagerHandler(7);
GeneralManagerHandler generalManagerHandler = new GeneralManagerHandler(Integer.MAX_VALUE);
LeaveRequestHandlerChain chain = new LeaveRequestHandlerChain();
chain.addHandler(supervisorHandler);
chain.addHandler(managerHandler);
chain.addHandler(generalManagerHandler);
return chain.process(leaveRequest);
}
}

零拷贝机制

类加载机制

一个 Java 程序运行,至少有三个类加载器实例,负责不同类的加载。

  1. Bootstrap loader 核心类库加载器

    C/C++实现,无对应 Java 类:null

    加载 JRE_HOME/jre/lib 目录,或用户配置的目录,JDK 核心类库 r t.jar…

  2. Extension Class Loader 扩展类库加载器

    ExtClassLoader 的实例:

    加载 JRE_HOME/jre/lib/ext 目录,JDK 扩展包,或用户配置的目录

  3. Application Class Loader 用户应用程序加载器

    AppClassLoader 的实例:

    加载 java.class.path 指定的目录,用户应用程序 class-path,或者 Java 命令运行时参数 -cp…

查看类对应加载器的方法:java.lang.Class.getClassLoader()

该方法返回装载类的类加载器,如果这个类是由 BootstrapClassLoader 加载的,那么返回 null。

不会重复加载

双亲委派模型

垃圾回收机制

标记算法

  • 引用计数
  • 可达性分析算法
引用类型
可达性级别

收集算法

JVM 命令工具

javap

jps

jstat

jcmd

jinfo

jhat

jmap

jstack

jconsole

JvisualVM

JVM 调优

Tomcat

Tomcat 网络处理线程模型

BIO + 同步 Servlet

Tomcat 7 及以前使用 BIO + 同步 Servlet,一个请求使用一个线程。

APR + 异步 Servlet

Apache可移植运行时(Apache Portable Runtime,简称APR)是Apache HTTP服务器的支持库,提供了一组映射到下层操作系统的API。

JNI 的形式调用 Apache HTTP 服务器的核心动态链接库来处理文件读取或网络传输操作。

Tomcat 默认监听指定路径,如果有 APR 安装,则自动启动。

NIO + 异步 Servlet

Tomcat 8 开始默认使用 NIO

Tomcat 参数调优

中间件

消息中间件

AMQP 协议

高级消息队列协议即Advanced Message Queuing Protocol是面向消息中间件提供的开放的应用层协议,其设计目标是对于消息的排序、路由、保持可靠性、保证安全性。 维基百科

MQTT 协议

MQTT消息队列遥测传输(Message Queuing Telemetry Transport)是ISO 标准下基于发布 /订阅 范式的消息协议,可视为“资料传递的桥梁”它工作在TCP/IP协议族上,是为硬件性能低下的远程设备以及网络状况糟糕的情况下而设计的发布/订阅型消息协议,为此,它需要一个消息中间件,以解决当前繁重的资料传输协议,如:HTTP。 维基百科

OpenMessaging 协议

首个由国内发起的分布式消息领域的国际标准OpenMessaging …

Kafka 协议

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。其持久化层本质上是一个“按照分布式事务日志架构的大规模发布/订阅消息队列”,这使它作为企业级基础设施来处理流式数据非常有价值。 维基百科

Kafka 协议是居于 TCP 的二机制协议。消息内部是通过长度来分割,由一些基本数据类型组成。

消息持久化

ActiveMQ RabbitMQ Kafka RocketMQ
文件系统 支持 支持 支持 支持
数据库 支持 / / /

消息分发策略

ActiveMQ RabbitMQ Kafka RocketMQ
文件系统 支持 支持 支持 支持
轮询分发 支持 支持 支持 /
公平分发 / 支持 支持 /
重发 支持 支持 / 支持
消息拉取 / 支持 支持 支持

集群

Master-Slave 主从共享部署方式

Master-Slave 主从同步部署方式

Broker-Cluster 多主集群同步部署方式

Broker-Cluster 多主集群转发部署方式

ActiveMQ

Apache ActiveMQ是Apache软件基金会所研发的开放源码消息中间件;由于ActiveMQ是一个纯Java程序,因此只需要操作系统支持Java虚拟机,ActiveMQ便可执行。 维基百科

JMS 规范

Java消息服务应用程序接口是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。 Java消息服务的规范包括两种消息模式,点对点和发布者/订阅者。 维基百科

JMS 对象模型

JMS 消息模型

  • 点对点

  • 发布订阅

JMS 消息结构

  • 消息头
  • 消息属性
  • 消息体类型

ActiveMQ 高可用集群方案

ActiveMQ 持久化

JDBC 方式

性能低

AMQ 方式

KahaDB 方式

LevelDB 方式

已被摒弃

事务机制

Nginx

Nginx 不执行动态语言,只能作为静态资源服务器。

负载均衡

  1. 轮询
  2. 最少连接
  3. IP hash
  4. 基于权重

代理缓存

通过 Lua 拓展 Nginx

Lua 使得 Nginx 可以支持动态语言。

协程(Coroutine)

Nginx 进程模型

OpenResty

OpenResty是基于nginx的Web平台,可以使用其LuaJIT引擎运行Lua脚本。该软件是由Yichun Zhang创建的。它最初是在2011年前由Taobao.com赞助的,在2012年至2016年期间主要由Cloudflare支持。 维基百科(英文)

高性能 Nginx 最佳实践

监听端口

虚拟主机

配置 location

常规配置

定义环境变量
嵌入其他配置文件
pid文件
worker 进程运行的用户和用户组
指定 worker 进程可以打开的最大句柄描述符个数
限制信号队列

高性能配置

worker 进程个数
绑定 worker 进程到指定 CPU 内核
SSL 硬件加速
worker 进程优先级设置

事件配置

是否打开 accept 锁
使用 accept 锁后到真正建立连接之间的延迟时间
批量建立新连接
选择事件模型
每个 worker 的最大连接数

Nginx 事件模型

epoll

LVS 负载均衡软件

Linux虚拟服务器(Linux Virtual Server)是一个虚拟的服务器集群系统,用于实现负载平衡。项目在1998年5月由章文嵩成立,是中国国内最早出现的自由软件项目之一。 维基百科

IP 虚拟服务器软件 IPVS

Virtual Server via Direct Routing (VS/DR)
Virtual Server via Network Address Translation (VS/NAT)
Virtual Server via IP Tunneling (VS/TUN)

IPVS 调度算法

八种负载调度算法

内核 Layer-7 交换机 KTCPVS

LVS 与 Nginx 对比

基于 VIP 的 keepalived 高可用架构

Keepalived

工作原理

应用场景

高可用集群

高可用架构

使用 CDN 实现应用的缓存和加速

CDN

服务模式

工作流程

关键技术

缓存

谷歌 Guava 缓存

Guava Cache 是 Google Guava 中的一个内存缓存模块,用于将数据缓存到 JVM 内存中。

Spring Cache

Redis

常用命令

数据结构

GEO

Stream

持久化

RDB

AOF

内存管理

内存分配

  • String 类型的 value 最大可存储 512M。
  • List 类型,元素个数最多 2^32-1
  • Set 类型,元素个数最多 2^32-1
  • Hash 类型,键值对个数最多 2^32-1