开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java 多线程 勉强弄懂了AQS

发表于 2021-03-15

欢迎大家关注相关的文档

总文档 :文章目录

Github : github.com/black-ant

一 . AQS 基础

一句话概括AQS :

  • 一个叫 AbstractQueuedSynchronizer 的抽象类
  • 包含2个重要概念 : 以Node为节点实现的链表的队列(CHL队列) + STATE标志
  • 支持2种锁 : 独占锁和共享锁 ,

1.1 什么是 AQS ?

java.util.concurrent.locks.AbstractQueuedSynchronizer 抽象类,简称 AQS , 队列同步器

作用 : 用于构建锁和同步容器的同步器

原理 : 使用一个 FIFO 的队列表示排队等待锁的线程

  • 队列头节点称作“哨兵节点”或者“哑节点”,不与任何线程关联。
  • 其他的节点与等待线程关联,每个节点维护一个等待状态 waitStatus

场景 : AQS解决了实现同步器时涉及当的大量细节问题,例如获取同步状态、FIFO同步队列


1.2 AQS 的 status 表示了什么 ?

AQS 使用一个int 的 status 来表示同步状态, 同步状态重要目的是用于跟踪线程是否应该阻塞 , 当它的前身释放时,一个节点被通知。否则,队列中的每个节点都充当一个特定通知样式的监视器,该监视器持有单个等待线程.

  • status > 0 : 获取了锁
  • status = 0 : 释放了锁
  • status < 0 :

1.3 常用方法:

状态处理

  • getState():返回同步状态的当前值。
  • setState(int newState):设置当前同步状态。
  • compareAndSetState(int expect, int update):使用 CAS 设置当前状态,该方法能够保证状态设置的原子性。

独占锁相关方法

  • 【可重写】#tryAcquire(int arg):独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态。
  • 【可重写】#tryRelease(int arg):独占式释放同步状态。

共享锁相关方法

  • 【可重写】#tryAcquireShared(int arg):共享式获取同步状态,返回值大于等于 0 ,则表示获取成功;否则,获取失败。
  • 【可重写】#tryReleaseShared(int arg):共享式释放同步状态。
  • 【可重写】#isHeldExclusively():当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占。

独占式获取同步状态

  • acquire(int arg):独占式获取同步状态。
    • 如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 #tryAcquire(int arg) 方法;
  • acquireInterruptibly(int arg):与 #acquire(int arg) 相同,但是该方法响应中断。
    • 当前线程为获取到同步状态而进入到同步队列中
    • 如果当前线程被中断,则抛出 InterruptedException()
    • 如果未中断 ,将尝试调用 tryAcquire , 调用失败线程将进入队列,可能会反复阻塞和解除阻塞
  • tryAcquireNanos(int arg, long nanos):超时获取同步状态。
    • 如果当前线程被中断,则抛出 InterruptedException()
    • 如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true 。
    • 未超时未获取会一只排队 ,反复阻塞

共享式获取同步状态

  • acquireShared(int arg):共享式获取同步状态
    • 如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;
  • acquireSharedInterruptibly(int arg):共享式获取同步状态,响应中断。
  • tryAcquireSharedNanos(int arg, long nanosTimeout):共享式获取同步状态,增加超时限制。

释放同步状态

  • release(int arg):独占式释放同步状态
    • 该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒。
  • releaseShared(int arg):共享式释放同步状态。
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
java复制代码public class SimpleLock extends AbstractQueuedSynchronizer {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
protected boolean tryAcquire(int unused) {
logger.info("------> try tryAcquire :{} <-------", unused);
//使用compareAndSetState控制AQS中的同步变量
if (compareAndSetState(0, 1)) {
logger.info("------> cas success ");
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

@Override
protected boolean tryRelease(int unused) {
logger.info("------> try tryRelease :{} <-------", unused);
setExclusiveOwnerThread(null);
//使用setState控制AQS中的同步变量
setState(0);
return true;
}

public void lock() {
acquire(1);
}

public boolean tryLock() {
return tryAcquire(1);
}

public void unlock() {
release(1);
}

}

其他关联知识点 :
1 AQS的所有子类中,要么使用了它的独占锁,要么使用了它的共享锁,不会同时使用它的两个锁。

二 . AQS 原理

2.1 基本原理

AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

  • AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。
  • AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
  • 在await之后,一个节点被插入到条件队列中(可见后面代码)。收到信号后,节点被转移到主队列

CLH(Craig,Landin,and Hagersten)队列
该队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。

AQS定义两种资源共享方式
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:

  • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
  • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的

Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。

2.1 AQS底层使用了模板方法模式

2.1.1 模板方法详情

同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):

  1. 使用者继承AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源state的获取和释放)
  2. 将AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。

这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。

AQS使用了模板方法模式,自定义同步器时需要重写下面几个AQS提供的模板方法:

以下方法未重写抛出 UnsupportedOperationException

  • isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

2.1.2 常见的实现案例

Semaphore(信号量)

功能 : 允许多个线程同时访问

synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。

CountDownLatch (倒计时器)
功能 : CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。

这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

CyclicBarrier(循环栅栏)

功能 : CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。

应用场景 : 和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。

作用 : 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。

三 . AQS 同步状态的获取和释放

独占式获取和释放同步状态

  • 同一时刻,仅有一个线程持有同步状态
  • acquire(int arg) : 该方法对中断不敏感 , 即后续对该线程进行中断操作时,线程不会从 CLH 同步队列中移除
  • tryAcquire(int arg) : 去尝试获取同步状态
    • true : 获取成功 , 设置锁状态 , 直接返回不用线程阻塞 , 自旋直到获得同步状态成功
    • false : 获取失败 , 用#addWaiter(Node mode) 方法,将当前线程加入到 CLH 同步队列尾部,mode 方法参数为Node.EXCLUSIVE
  • acquireQueued : 自旋直到获得同步状态成功
1
2
3
4
5
java复制代码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

共享式获取和释放同步状态

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
java复制代码
// 首先调用至少一次tryacquisharered
//
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}


// 上述调用失败 , 线程可能会进入队列反复阻塞和解除阻塞
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire 检查并更新未能获取的节点的状态。如果线程阻塞,则返回true
// parkAndCheckInterrupt : 中断线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

shouldParkAfterFailedAcquire 的作用

  • 等待状态为 Node.SIGNAL 时,表示 pred 的下一个节点 node 的线程需要阻塞等待。在pred 的线程释放同步状态时,会对 node 的线程进行唤醒通知
  • 等待状态为 0 或者 Node.PROPAGATE 时,通过 CAS 设置,将状态修改为 Node.SIGNAL
  • 等待状态为 NODE.CANCELLED 时,则表明该线程的前一个节点已经等待超时或者被中断了,则需要从 CLH 队列中将该前一个节点删除掉,循环回溯,直到前一个节点状态 <= 0

查询同步队列中的等待线程情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码 // 自旋处理流程: 
for (;;) {
// 获取当前线程的前驱节点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 当前线程的前驱节点是头结点,且同步状态成功
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 获取失败,线程等待
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}

四. 阻塞和唤醒线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码AQS 在处理中有2处值得深入的过程 : 阻塞和唤醒

// 阻塞发生在获取对应同步方法同步失败后 ,流程为 :
Step Start : 获取同步状态 -> 获取失败 -> 检查该状态 shouldParkAfterFailedAcquire(Node pred, Node node)
Step 2 : 返回true -> 当前线程应该被柱塞
Step 3 : parkAndCheckInterrupt() 阻塞线程
- 调用 LockSupport#park(Object blocker) 方法,将当前线程挂起,此时就进入阻塞等待唤醒的状态

// 后续将进行线程的唤醒操作 , 唤醒分为2种
第一种,当前节点(线程)的前序节点释放同步状态时,唤醒了该线程
第二种,当前线程被打断导致唤醒。

Step Start : 当线程释放同步状态后 , 唤醒该线程的后继节点 (unparkSuccessor)
Step 2 : 后继节点存在 , 唤醒后继节点 LockSupport.unpark(s.thread)
Step 3 : 如果后继节点为null (超时 , 中断) , 通过 tail 回溯的方式找到最前序的可用的线程

// 补充 :
> park() : 阻塞当前线程
> park(Object blocker) : 为了线程调度 , 在许可可用之前兼用当前线程
> unpark : 如果给定线程的许可尚不可用 , 则使其可用
> parkNanos(long nanos) :为了线程调度禁用当前线程,最多等待指定的等待时间,除非许可可用

- park 方法和 unpark(Thread thread) 方法,都是成对出现的
- unpark(Thread thread) 方法,必须要在 park 方法执行之后执行

五 . CLH 同步队列

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
java复制代码> 简介 : CLH 同步队列是一个 FIFO 双向队列,AQS 依赖它来完成同步状态的管理

> 2种状态 :
• 当线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
• 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

C- Node : AbstractQueuedSynchronizer 的内部静态类
SF- Node SHARED = new Node();
SF- Node EXCLUSIVE = null;
SF- int CANCELLED = 1;
SF- int SIGNAL = -1;
SF- int CONDITION = -2;
SF- int PROPAGATE = -3;
F- volatile int waitStatus -- CANCELLED SIGNAL CONDITION PROPAGATE INITAL 总共 5 种状态 , 其中 INITAL 是初始状态
F- volatile Node prev; -- 指向前一个节点
F- volatile Node next; -- 指向后一个节点
F- volatile Thread thread; -- Node 节点对应的线程 Thread
F- Node nextWaiter; -- Node 节点获取同步状态的模型( Mode )
M- tryAcquire : 独占式获取同步状态
M- tryAcquireShared : 共享式获取同步状态
M- addWaiter : 入队
M- isShared : 判断是否为共享式获取同步状态
M- predecessor : 获得 Node 节点的前一个 Node 节点


// 属性详情 :
Node 中包含链表常用的2个概念 : prev , next , 同步器中包含2个属性 head , tail 分别指向队列的头和尾 ,


> 入列 :
M- addWaiter
- 准备新节点 -> 记录尾节点 -> 将新节点放入尾节点 -> CAS 设置新的尾节点
- 失败反复尝试 , 直到成功


> 出列 :
- 首节点的线程释放同步状态后,将会唤醒它的下一个节点(Node.next)。后继节点将会在获取同步状态成功时,将自己设置为首节点( head )
- setHead
- 该操作为单线程操作

// 原理简述 :

六. AQS 源码

9.6.1 问题一 : CLH 的形式

AQS 有2个属性

  • private transient volatile Node head;
    • 等待队列的头,懒加载。初始化后只能通过sehead方法进行修改。
    • 注意:如果head存在,它的waitStatus保证不存在
  • private transient volatile Node tail;
    • 尾部的等待队列,懒加载

AQS 有几个重要的方法

  • private Node addWaiter(Node mode)
    • 为当前线程和给定模式创建并进入节点队列
  • private void setHead(Node node)
    • 设置队列头为节点,退出队列。
    • 仅通过acquire方法调用。
    • 将未使用的字段置空 (GC 及效率)
  • private Node enq(final Node node)
    • 将节点插入队列,必要时进行初始化

9.6.2 问题二 : Node 节点

AQS 中有个内部类 Node , 他是节点对象 , 它其中有四个属性表示状态

  • SIGNAL :该节点的后继节点被阻塞(或即将被阻塞)(通过park),因此当前节点在释放或取消后继节点时必须解除它的后继节点的阻塞
  • CANCELLED : 该节点因超时或中断而被取消
  • CONDITION : 该节点当前在条件队列中。它将不会被用作同步队列节点,直到传输时,状态将被设置为0
  • PROPAGATE : 这个 releaseShared 应该传播到其他节点

它还有如下几个重要的属性

  • volatile Node prev : 前一个节点
  • volatile Node next : 下一个节点
  • volatile Thread thread : 当前线程

9.6.3 AQS 状态

  • private volatile int state;

9.6.5 AQS 流程图

七 . AQS 使用

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
java复制代码@ https://github.com/black-ant/case/tree/master/case%20Module%20Thread/case%20AQS

public class SimpleLock extends AbstractQueuedSynchronizer {

@Override
protected boolean tryAcquire(int unused) {
//使用compareAndSetState控制AQS中的同步变量
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}

@Override
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
//使用setState控制AQS中的同步变量
setState(0);
return true;
}

public void lock() {
acquire(1);
}

public boolean tryLock() {
return tryAcquire(1);
}

public void unlock() {
release(1);
}

}

// 分析
1 [ main] try tryAcquire :1 <-------
2 [ main] cas success
3 [ Thread-52] try tryAcquire :1 <-------
4 [ Thread-52] try tryAcquire :1 <-------
5 [ Thread-52] try tryAcquire :1 <-------
6 [ Thread-53] try tryAcquire :1 <-------

16 [ main] try tryRelease :1 <-------
17 [ Thread-52] try tryAcquire :1 <-------
18 [ Thread-52] cas success
19 [ Thread-52] c.g.s.thread.aqs.demo.logic.StartLogic : ------> acquired the lock! <-------
20 [ Thread-52] try tryRelease :1 <-------
21 [ Thread-53] try tryAcquire :1 <-------
22 [ Thread-53] cas success
23 [ Thread-53] c.g.s.thread.aqs.demo.logic.StartLogic : ------> acquired the lock! <-------
24 [ Thread-53] try tryRelease :1 <-------


// 第2行 : main 线程获取了独占锁 , 导致后续3-6行的线程全部无法获取锁 , 排在队列中
// 第16行 : main 释放了锁 , 所以从 17-20 行 ,是 Thread-52 的操作流程 (后面可以看到 53 的队列流)



// AQS 使用如上文所示 , 通常要实现 tryAcquire 和 tryRelease
TODO

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java 多线程 漫谈多线程模型 一 happens-

发表于 2021-03-15

总文档 :文章目录

Github : github.com/black-ant

一 . happens-before 模型

1.1 happens-before 定义

happens-before 规则可以帮助我们有效的判断操作的顺序 , 帮助我们理解多线程的数据流动

  1. 如果一个操作 happens-before 另一个操作,那么第一个操作的执行结果,将对第二个操作可见,而且第一个操作的执行顺序,排在第二个操作之前。
  2. 两个操作之间存在 happens-before 关系,并不意味着一定要按照 happens-before 原则制定的顺序来执行。如果重排序之后的执行结果与按照 happens-before 关系来执行的结果一致,那么这种重排序并不非法。

1.2 happens-before 规则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作,happens-before 于书写在后面的操作。
  • 锁定规则:一个 unLock 操作,happens-before 于后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个变量的写操作,happens-before 于后面对这个变量的读操作。
  • 传递规则:如果操作 A happens-before 操作 B,而操作 B happens-before 操作C,则可以得出,操作 A happens-before 操作C
  • 线程启动规则:Thread 对象的 start 方法,happens-before 此线程的每个一个动作。
  • 线程中断规则:对线程 interrupt 方法的调用,happens-before 被中断线程的代码检测到中断事件的发生。
  • 线程终结规则:线程中所有的操作,都 happens-before 线程的终止检测,我们可以通过Thread.join() 方法结束、Thread.isAlive() 的返回值手段,检测到线程已经终止执行。
  • 对象终结规则:一个对象的初始化完成,happens-before 它的 finalize() 方法的开始

1.3 其他规则

  1. 将一个元素放入一个线程安全的队列的操作,happens-before 从队列中取出这个元素的操作。
  2. 将一个元素放入一个线程安全容器的操作,happens-before 从容器中取出这个元素的操作。
  3. 在 CountDownLatch 上的 countDown 操作,happens-before CountDownLatch 上的 await 操作。
  4. 释放 Semaphore 上的 release 的操作,happens-before 上的 acquire 操作。
  5. Future 表示的任务的所有操作,happens-before Future 上的 get 操作。
  6. 向 Executor 提交一个 Runnable 或 Callable 的操作,happens-before 任务开始执行操作。

二 . 重排序

为了提高性能,处理器和编译器常常会对指令进行重排序 , 其主要目的是在不改变程序执行结果的前提下,尽可能提高程序的运行效率

  1. 在单线程环境下,不能改变程序运行的结果。
  2. 存在数据依赖关系的情况下,不允许重排序。
    • 多线程情况下 , 当代码中存在控制依赖性时,会影响指令序列的执行的并行度,所以编译器和处理器会采用猜测执行来克服控制依赖对并行度的影响

总结 : 无法通过 happens-before 原则推导出来的,JMM 允许任意的排序。

as-if-serial 语义

  • 所有的操作均可以为了优化而被重排序,但是你必须要保证重排序后执行的结果不能被改变,编译器、runtime、处理器都必须遵守 as-if-serial 语义
  • as-if-serial 只保证单线程环境,多线程环境下无效

JIT 优化原则
尽可能地优化程序正常运行下的逻辑,哪怕以 catch 块逻辑变得复杂为代价。

三 . 线程的 CPU 时间片

Java中线程会按优先级分配CPU时间片运行

  1. 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
  2. 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上。
  3. 当前运行线程结束,即运行完run()方法里面的任务。

yield 放弃 CPU
yield操作并不会永远放弃CPU,仅仅只是放弃了此次时间片,把剩下的时间让给别的线程

IO 柱塞
运行程序将有两条线程工作,ioThread每次遇到I/O阻塞就放弃当前的时间片,而主线程则按JVM分配的时间片正常运行

CPU 优先级
Java把线程优先级分成10个级别,线程被创建时如果没有明确声明则使用默认优先级,JVM将根据每个线程的优先级分配执行时间的概率。有三个常量 :

  • Thread.MIN_PRIORITY : 最小优先级值(1)
  • Thread.NORM_PRIORITY : 默认优先级值(5)
  • Thread.MAX_PRIORITY : 最大优先级值(10)。

线程的调度策略决定上层多线程运行机制,JVM的线程调度器实现了抢占式调度,每条线程执行的时间由它分配管理,它将按照线程优先级的建议对线程执行的时间进行分配,优先级越高,可能得到CPU的时间则越长。

四 .内存屏障

硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力

几种主要的内存屏障 :

  1. lfence,是一种Load Barrier 读屏障
  2. sfence, 是一种Store Barrier 写屏障
  3. mfence, 是一种全能型的屏障,具备ifence和sfence的能力
  4. 带Lock前缀类,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。
    • Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
    • 它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

内存屏障的能力

  1. 阻止屏障两边的指令重排序
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效

**内存屏障最常见的地方就是 Volatile **

五. CPU

CPU 高速缓存原理
Java内存模型中每个线程的工作内存实际上就是寄存器以及高速缓存的抽象 , 各个核心直接通过系统总线连接 , 而总线是一种共享资源 , 这就意味着资源竞争.

计算机的局部性原理
局部性原理是缓存技术的底层理论基础。局部性包括两种形式:

  1. 时间局部性,一个具有良好时间局部性的程序中,被引用过一次的存储器位置很可能在不远的将来再被多次引用
  2. 空间局部性,一个具有良好空间局部性的程序中,如果一个存储器位置被引用了一次,那么程序很可能在不远的将来引用附近的一个存储器位置

存储器体系结构 :
存储器呈现金字塔结构 , 主要包括寄存器 , 高速缓存 , 主存等几个概念 , 存储器有以下几个特点 :

  • 一层存储器只和下层存储器打交道,不会跨级访问
  • 下层作为上层的一个缓存。CPU要访问的数据的最终一般都经过主存,主存作为下层其他设备的一个缓存,其他设备的数据最终都要进入主存才能被CPU访问到

CPU 高速缓存核心原理
TODO @ blog.csdn.net/iter_zc/art…

六. 内存模型之一致性

一致性的满足 @ blog.csdn.net/iter_zc/art…

  • 在单机器多CPU的情况下,多CPU并发执行,共用一个内存,一般通过共享内存的方式来处理一致性问题,通过定义满足不同一致性需求的内存模型来解决内存一致性问题(Memory Consistency)
  • 在分布式环境中,多台机器多CPU通过网络来并发执行,一般通过消息通信的方式来处理一致性问题 (Paxos协议 , Zab协议)

6.1 一致性分类

严格一致性 Strict Consistency 线性一致性 Linearizability

所有的读写操作都按照全局的时序来排列执行 , 所有的CPU需要共享一个全局的时钟顺序 , 且按照该顺序执行

顺序一致性 Sequential Consistency

  • 对每个单个CPU来说,它看到自己程序的执行顺序始终是和程序定义是一致的(单个CPU角度)
  • 每个CPU看到的其他CPU的写操作都是按照相同的顺序执行的,大家看到的最终执行的视图是一致的(从全局的角度)
  • 单个CPU对共享变量的写操作马上对其他CPU可见

因果一致性 Causal Consistency

  • 因果一致性是一种弱的顺序一致性,只有有因果关系的数据才需要保证顺序一致性,没有因果关系的数据不需要保证顺序一致性
  • 通俗来说就是 B 对 x 的写操作 W(x)B 会依赖于 A 对 x 的写操作 , 即对外展现为 W(x)a, W(x)b

处理器一致性/ PRAM(Piplined RAM) 管道式存储器

  • 只要求从一个处理器来的写操作按照同样的顺序被其他处理器看到,不同处理器的写操作可以按照不同的顺序被看到
  • 就是说它不保证有因果关系的写操作按照执行的顺序执行

弱一致性 Weak Consistency

弱一致性只对被同步操作保护的共享变量而言,规定了只有对共享变量的同步操作完成之后,共享数据才可能保持一致性.在同步操作过程中,是不保证一致性的,单个处理器对共享变量的修改对其他处理器是不可见的。相比与严格的顺序一致性,它只保持了执行顺序上的顺序一致性,至于可见性必须要等待同步操作结束

  • 对同步变量的读写按照顺序一致性
  • 只有所有对同步变量的写操作完成之后才能对同步变量进行访问
  • 只有所有对同步变量的访问(读/写)完成后才能对同步变量访问

释放一致性 Release Consistency

  • 释放一致性规定了对同步变量的释放操作后,就对同步变量的状态广播到其他处理器

进入一致性 Entry Consistency

  • 进入同步变量时,获取同步变量的最新状态

缓存一致性 Cache Consistency

  • TODO

九 . 多线程模型

9 .1 并行模型

并行 Worker : (许多 java.util.concurrent 包下的并发工具都使用了这种模型)

并行 worker 的核心思想是,它主要有两个进程即代理人和工人,Delegator 负责接收来自客户端的任务并把任务下发,交给具体的 Worker 进行处理,Worker 处理完成后把结果返回给 Delegator,在 Delegator 接收到 Worker 处理的结果后对其进行汇总,然后交给客户端。

9 .2 响应式 - 事件驱动系统 : Actor 模型

在 Actor 模型中,每一个 Actor 其实就是一个 Worker, 每一个 Actor 都能够处理任务。

简单来说,Actor 模型是一个并发模型,它定义了一系列系统组件应该如何动作和交互的通用规则,最著名的使用这套规则的编程语言是 Erlang。一个参与者Actor对接收到的消息做出响应,然后可以创建出更多的 Actor 或发送更多的消息,同时准备接收下一条消息。

9 .3 响应式 - 事件驱动系统 : Channels 模型 .

在 Channel 模型中,worker 通常不会直接通信,与此相对的,他们通常将事件发送到不同的 通道(Channel)上,然后其他 worker 可以在这些通道上获取消息,下面是 Channel 的模型图

致谢

欢迎大家关注我的相关文档

多线程集合目录

芋道源码

死磕系列

JVM 源码

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

京东数科面试真题:常见的 IO 模型有哪些?Java 中的

发表于 2021-03-15

本文节选自《Java面试进阶指北 打造个人的技术竞争力》

面试中经常喜欢问的一个问题,因为通过这个问题,面试官可以顺便了解一下你的操作系统的水平。

IO 模型这块确实挺难理解的,需要太多计算机底层知识。写这篇文章用了挺久,就非常希望能把我所知道的讲出来吧!希望朋友们能有收货!为了写这篇文章,还翻看了一下《UNIX 网络编程》这本书,太难了,我滴乖乖!心痛~

个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!

前言

I/O 一直是很多小伙伴难以理解的一个知识点,这篇文章我会将我所理解的 I/O 讲给你听,希望可以对你有所帮助。

I/O

何为 I/O?

I/O(Input/Outpu) 即输入/输出 。

我们先从计算机结构的角度来解读一下 I/O。

根据冯.诺依曼结构,计算机结构分为 5 大部分:运算器、控制器、存储器、输入设备、输出设备。

冯诺依曼体系结构

输入设备(比如键盘)和输出设备(比如显示屏)都属于外部设备。网卡、硬盘这种既可以属于输入设备,也可以属于输出设备。

输入设备向计算机输入数据,输出设备接收计算机输出的数据。

从计算机结构的视角来看的话, I/O 描述了计算机系统与外部设备之间通信的过程。

我们再先从应用程序的角度来解读一下 I/O。

根据大学里学到的操作系统相关的知识:为了保证操作系统的稳定性和安全性,一个进程的地址空间划分为 用户空间(User space) 和 内核空间(Kernel space ) 。

像我们平常运行的应用程序都是运行在用户空间,只有内核空间才能进行系统态级别的资源有关的操作,比如如文件管理、进程通信、内存管理等等。也就是说,我们想要进行 IO 操作,一定是要依赖内核空间的能力。

并且,用户空间的程序不能直接访问内核空间。

当想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。

因此,用户进程想要执行 IO 操作的话,必须通过 系统调用 来间接访问内核空间

我们在平常开发过程中接触最多的就是 磁盘 IO(读写文件) 和 网络 IO(网络请求和相应)。

从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的调用而已,具体 IO 的执行是由操作系统的内核来完成的。

当应用程序发起 I/O 调用后,会经历两个步骤:

  1. 内核等待 I/O 设备准备好数据
  2. 内核将数据从内核空间拷贝到用户空间。

有哪些常见的 IO 模型?

UNIX 系统下, IO 模型一共有 5 种: 同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。

这也是我们经常提到的 5 种 IO 模型。

Java 中 3 种常见 IO 模型

BIO (Blocking I/O)

BIO 属于同步阻塞 IO 模型 。

同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。

图源:《深入拆解Tomcat & Jetty》

在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

NIO (Non-blocking/New I/O)

Java 中的 NIO 于 Java 1.4 中引入,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使用 NIO 。

Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。

跟着我的思路往下看看,相信你会得到答案!

我们先来看看 同步非阻塞 IO 模型。

图源:《深入拆解Tomcat & Jetty》

同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。

相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。

但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

这个时候,I/O 多路复用模型 就上场了。

IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。

目前支持 IO 多路复用的系统调用,有 select,epoll 等等。select 系统调用,是目前几乎在所有的操作系统上都有支持

  • select 调用 :内核提供的系统调用,它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。
  • epoll 调用 :linux 2.6 内核,属于 select 调用的增强版本,优化了 IO 的执行效率。

IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。

Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为 多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。

AIO (Asynchronous I/O)

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改进版 NIO 2,它是异步 IO 模型。

异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

目前来说 AIO 的应用还不是很广泛。Netty 之前也尝试使用过 AIO,不过又放弃了。这是因为,Netty 使用了 AIO 之后,在 Linux 系统上的性能并没有多少提升。

最后,来一张图,简单总结一下 Java 中的 BIO、NIO、AIO。

参考

  • 《深入拆解 Tomcat & Jetty》
  • 如何完成一次 IO:llc687.top/post/如何完成一次…
  • 程序员应该这样理解 IO:www.jianshu.com/p/fa7bdc4f3…
  • 10 分钟看懂, Java NIO 底层原理:www.cnblogs.com/crazymakerc…
  • IO 模型知多少 | 理论篇:www.cnblogs.com/sheng-jie/p…
  • 《UNIX 网络编程 卷 1;套接字联网 API 》6.2 节 IO 模型

推荐

《JavaGuide 面试突击版》PDF 版本 。图解计算机基础 PDF 版。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微信PC网站支付采坑记-微信Native支付申请接入 常见错

发表于 2021-03-15

常见错误 强烈建议阅读完再搞避免踩坑多花冤枉钱

1、该ID未完成微信认证,请确认后再试
完成appid的认证,微信服务号为企业认证,开放平台为开发者认证。
2、你输入的APPID认证主体名称与实际认证主体不一致,请检查修改后重试

检查开放平台或者公众平台认证主体和商户号主体是否一致。
3、不支持关联该类型的appID
微信Native支付(PC网站)不能用开放平台的appid.
微信Native支付的appid 必须为 微信服务号 即认证主体为企业的公众号。
微信Native支付的appid 必须为微信服务号 即认证主体为企业的公众号。
微信Native支付的appid 必须为微信服务号即认证主体为企业的公众号。
4、修改支付时提示的收款方名称
收款方名称要修改商户号里面的商户简称

微信Native支付申请流程

1、 申请微信商户号
2、 申请微信企业服务号并且认证,拿到appid
3、 将服务号的appid与商户号绑定
4、申请证书key之类的

微信Native支付接入

java开发 推荐使用IJPay

1、引入ijpay的库

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>2.7.3</version>
</dependency>

2、创建请求需要的参数类。配置信息在application中

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
java复制代码public class WxPayConfig {
/**
* 设置微信公众号或者小程序等的appid.
*/
@Value("${wx.pay.appId}")
private String appId;

/**
* 微信支付商户号.
*/
@Value("${wx.pay.mchId}")
private String mchId;

/**
* 微信支付商户密钥.
*/
@Value("${wx.pay.mchKey}")
private String mchKey;

/**
* 异步回调地址
*/
@Value("${wx.pay.notifyUrl}")
private String notifyUrl;
}

3、配置信息application.properties

1
2
3
4
5
6
java复制代码wxpay.appId=申请的服务号的appid
wxpay.appSecret=申请的服务号的appSecret
wxpay.mchId=申请的商户号
wxpay.partnerKey=商户号的partnerKey
wxpay.certPath=商户号p12证书
wxpay.domain=扫码支付完成的异步校验通知url

4、统一下单

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
java复制代码  Map<String, String> params = UnifiedOrderModel
.builder()
.appid(wxPayConfig.appId)
.mch_id(wxPayConfig.mchId)
.nonce_str(WxPayKit.generateStr())
.body("赵汤圆支付")
.attach("赵汤圆支付踩坑")
.out_trade_no(createOrderRequestDto.getId())
.total_fee(total_fee)
.spbill_create_ip(ip)
.notify_url(wxPayConfig.domain)
.trade_type(TradeType.NATIVE.getTradeType())
.build()
.createSign(wxPayConfig.partnerKey, SignType.HMACSHA256);
String xmlResult = WxPayApi.pushOrder(false, params);
Map<String, String> result = WxPayKit.xmlToMap(xmlResult);
String returnCode = result.get("return_code");
String returnMsg = result.get("return_msg");
System.out.println(returnMsg);
if (!WxPayKit.codeIsOk(returnCode)) {
// 异常状态判断 替换成你自己的
throw new ServiceException(ErrorTypeEnum.RECHARGE_ERROR);
}
String resultCode = result.get("result_code");
if (!WxPayKit.codeIsOk(resultCode)) {
// 异常状态判断 替换成你自己的
throw new ServiceException(ErrorTypeEnum.RECHARGE_ERROR);
}
//生成预付订单success
String qrCodeUrl = result.get("code_url");
try {
// 生成下单的二维码,返回给客户端显示
ByteArrayOutputStream out = new ByteArrayOutputStream();
QrCodeKit.encodeOutPutSteam(out, qrCodeUrl, BarcodeFormat.QR_CODE,
3, ErrorCorrectionLevel.H, "png", 200, 200);
BASE64Encoder encoder = new BASE64Encoder();
imgBase64Str = encoder.encode(out.toByteArray());
} catch (IOException e) {
e.printStackTrace();
}

return imgBase64Str;
5、异步通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
String xmlMsg = HttpKit.readData(request);
logger.info("支付通知=" + xmlMsg);
Map<String, String> params = WxPayKit.xmlToMap(xmlMsg);

String returnCode = params.get("return_code");

// 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
// 注意此处签名方式需与统一下单的签名类型一致
if (WxPayKit.verifyNotify(params, wxPayConfig.partnerKey, SignType.HMACSHA256)) {
if (WxPayKit.codeIsOk(returnCode)) {
// 更新订单信息
// 发送通知等
String out_trade_no = params.get("out_trade_no");
String total_fee = params.get("total_fee");
Map<String, String> xml = new HashMap<String, String>(2);
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
return WxPayKit.toXml(xml);
}
}

原创不易转载请注明出处

如果本文对你有所帮助,请帮作者买杯咖啡吧

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java实现国密电子签章(itext+bouncycastl

发表于 2021-03-15

**RSA数字签名:**rsa(hash(srcData))->填充pkcs#7格式->存储在pdf中

**SM2数字签名:**sm2(hash(srcData))->填充GB/T 38540格式->存储在pdf中

(在RSA体系中,一般来说第一步RSA对原文hash签名就是我们常说的裸签,裸签通过pkcs#7填充后就是我们pdf所需要的签名格式,其中有个pkcs#1概念就是裸签前对原文进行补位操作后再签名)

)​

电子签名、电子签章已经不是个陌生的东西了。在java领域使用itextpdf可以完成pdf的电子签名。当然最终是以pkcs7的格式存储在pdf文件中。类似

)​

我们知道pkcs7是RSA体系中的,属于国际标准。我们国家的国密标准也早就出台了,那是否能实现国密的数字签名呢?答案是肯定的可以。

方案一、

itextpdf+修改源代码+bouncycastle

1
2
3
4
5
ini复制代码MakeSignature.signDetached(appearance, digest, pks, 
chain, crlList, ocspClient, tsaClient, estimatedSize, subfilter);

修改MakeSignature类及相关的几个类,核心就是修改PdfPKCS7类。bouncycastle本身就支持
sm2 sm3等国密算法

方案二、

itextpdf+signExternalContainer+bouncycastle

1
2
3
4
5
ini复制代码ExternalSignatureContainer external = 
new HashExternalSignatureContainer(PdfName.ADOBE_PPKMS, PdfName.ADBE_PKCS7_DETACHED);
MakeSignature.signExternalContainer(appearance, external, 8192);

itextpdf已经提供signExternalContainer来让用户自定义签名

当然还有个关键问题就是如何把pkcs7换成国密的数据结构,我们知道国密的数据结构是asn.1编码的,当然pkcs7也是asn.1编码,只不过两者的数据结构体不一样。在java中可以使用bouncycastle来完成签名后的数据序列化成国密要求的数据结构。

step1

先按照《安全电子签章密码技术规范》国密标准生成java对象

)​

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scala复制代码@Data
@Builder
public class SESHeader extends ASN1Object {
//头标识
private DERIA5String id;
//印章版本号
private ASN1Integer version;
//厂商标识
private DERIA5String vid;


@Override
public ASN1Primitive toASN1Primitive() {
ASN1EncodableVector vector = new ASN1EncodableVector();
vector.add(id);
vector.add(version);
vector.add(vid);
return new DERSequence(vector);
}
}

step2

创建好对象后,就可以序列化成byte[]

1
ini复制代码byte[] encode = signature.toASN1Primitive().getEncoded();

step3

生成16进制就可以放置在pdf中了

1
scss复制代码Hex.toHexString(paddedSig)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

后端进阶图 后端学习路线思维导图 总结

发表于 2021-03-15

后端学习路线思维导图

server.png

总结

在后端一年半的学习和实践中汇总了后端的学习思维导图,希望各位大佬补充下并给出建议,一起进步

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nodejs前景与优劣比对

发表于 2021-03-15

!非专业文章,文章大部分内容仅为原文章的译文,仅供参考。欢迎批评和指点!

Node.js前景概述

Stack Overflow 2020年开发者调查1的结果显示,Node.js在网页应用开发工具和框架(the most popular web framework for Web app development)荣登榜首,获得51.9%的投票率。2020年开始,越来越多的公司迅速转型Node.js,如雅虎、沃尔玛、Uber等。

img

选择Node.js的原因

Node.js帮助企业解决一系列生产过程中的问题和挑战,其中有一部分是平台相关的(platform-related),还有一部分与资源利用(resource utilization)有关。Netflix、Paypal、Linkedin等都表示Node.js可以节省60%以上的启动时间2。

下图是开发者对于Node.js对开发影响的统计数据。可以看到,大部分开发者认为Node.js能提高开发速度,减少开发支出,以及提升应用的性能。此外,22%的开发者认为使用了Node.js后更容易招聘到合适的员工,这说明Node.js比其他语言(如Java、PHP、.NET)更容易上手,相比于Java的Spring,更加轻量和简易。SIMFORM做了一个调查报告,列举了15家公司对Node.js的看法3。

img

Node.js 的优势

img

高性能的实时应用

Node.js的一大优势就是可以快速地构筑(build)应用并显示结果。Node.js之所以强大,是因为它以谷歌浏览器(Chrome’s V8)引擎为基础,并使用C++编写,它以超高的效率将函数编译为机器码。为了保证持续的提升,谷歌采用V8基准套件来保证更快的执行速度、更优秀的编译器、安全补丁和高可伸缩性(scalability),进而提升Node.js的性能。

更易伸缩的现代应用

前沿的技术带来数以千计的特性,如集群模块(cluster module)。它促进了多核CPU的负载均衡,使其通过更小的模块来实现需求变得更容易,且不会烧坏内存进程。此外,Node.js采用非阻塞时间循环机制,提高了可伸缩性,使服务器能无缝地处理请求。

从发展的眼光看,Node.js可以很好地支持微服务,让程序被划分为更小的部分。Node.js和微服务允许现代应用程序根据需求进行伸缩,帮助公司使用更少的资源实现更高的性能。

容易学习和快速上手

Javascript是WEB开发人员社区中最流行、使用最广泛的编程语言之一,适应和学习Node.js对于WEB开发者而言非常轻松,编写代码所需的时间更少。根据Node.js 2016年用户调查4,Javascript是最受欢迎的前端开发语言,学习和使用Node.js需要的时间和经历更少。根据StackOverflow最近的一项调查,参与投票的专业开发人员中有惊人的49.9%选择Node.js作为最常用的技术。

Node.js 的不足

img

  1. 在处理复杂的计算任务时性能下降:当Node.js事件循环中接收到相当大的CPU驱动的任务时,它会使用动用全部的CPU资源去完成之。(可以用其他语言完成Node.js不擅长的复杂计算。)
  2. API不稳定导致沉重的代码更迭开销:Node.js经常不向下兼容,当更新到新版本时,需要进行大幅度的改动。(Node.js是比较新的语言,仍然在蓬勃发展中。)
  3. 异步编程导致代码难以维护:四个字——回调地狱(callback hell)。(注:现在已经被Promise解决,但在没有出现Promise前一些库可能会出现这种问题,建议用较新的、长期维护的库。)
  4. 缺乏库的支持可能影响代码质量:NPM上有很多低质量和烂尾的库,并且缺乏相应的文档,因此可能导致产品的代码混乱和不规范。

Footnotes

  1. insights.stackoverflow.com/survey/2020 ↩
  2. hackernoon.com/how-netflix… ↩
  3. www.simform.com/companies-u… ↩
  4. nodejs.org/static/docu… ↩

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

老板:我需要个抽奖的系统,你来搞一下 前文 准备工作 小试牛

发表于 2021-03-15

前文

因为临近年底了,项目大部分都快结束了,难得的开启了快乐的摸鱼时光,可是🐟还没开始摸几天,老板就来了个任务:我们要搞活动,现在需要一个抽奖系统,你来搞一下。

好的,🐟还没摸两条,又要开始干了,说干就干。

准备工作

这次的系统我用的是我最爱的nodeJs来完成,用到的框架也就是Koa2,其余的数据库的话,我用的是mySql,还有Redis,先来一份思维导图,捋清楚整个业务流程

大概奖品分为四种,一种是大奖(iphone12 pro max),还有小奖(苹果耳机),以及虚拟卷,还有一些支付代币,然后在抽奖之前,会对用户ID和IP地址进行今天的抽奖次数的相关验证,以及用户黑名单的验证,如果这个已经中过大奖的情况下,这个用户ID和IP地址则会被拉黑,接下来一段时间内的抽奖都将不会在中奖或者说只能中虚拟币这样的奖,最后,如果中奖的话,会有一个对于这个用户的中奖记录。

好了,貌似业务也不是特别复杂,so easy,项目初始化,model建起来,数据表创起来,搞起。

小试牛刀第一步

通过把基础的奖品什么的增删改查创建好,我大概说明一下我的奖品列表在刚开始创建的时候的样子,这里我就通过prize的model来展示它最初的样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
css复制代码title: { type: DataTypes.STRING, comment: '奖品名称' },
prize_num: { type: DataTypes.INTEGER, comment: '奖品数量' }, //0 无奖品,>0限量,<0无限量
left_num: { type: DataTypes.INTEGER, comment: '剩余奖品数量' },
prize_code: { type: DataTypes.STRING, comment: '中奖的概率' }, //0~9999
prize_time: { type: DataTypes.INTEGER, comment: '发奖周期' }, //抽奖活动持续多少天
img: { type: DataTypes.STRING, comment: '奖品图片' },
displayOrder: { type: DataTypes.INTEGER, comment: '位置序号' }, //小的排在前面
gType: {
type: DataTypes.INTEGER,
comment: '奖品类型',
}, //3 虚拟币,2 虚拟卷,1 实物 小,0 实物 大
sys_status: {
type: DataTypes.INTEGER,
comment: '状态',
}, // 0 正常 1 删除
time_begin: { type: DataTypes.DATE, comment: '开始时间' },
time_end: { type: DataTypes.DATE, comment: '结束时间' },

然后在抽奖的时候,会先对有各种乱七八糟的验证,这里可以通过我的路由来说明整体的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
php复制代码router.use(Token.checkToken)

//用户参与次数验证
router.use(usersignService.checkUser)

//ip参与次数验证
router.use(ipService.checkIp)

//ip黑名单
router.use(ipBlacklistService.checkIp)

//用户黑名单
router.use(blacklistService.checkUser)

//抽奖接口
router.get('/getPrize', luckyConstructor.prizeGet)

//礼品的颁发
router.use(couponService.setCoupon)

//中奖纪录
router.use(recodingsService.createData)

这个大概就是业务的整体的一个流程走向,那么下面就是一个抽奖的环节了。

因为本菜鸡算法水平有限,网上那些抽奖算法实在是看不懂,就用了最简单粗暴的方式,通过生成随机数的方式来进行匹配。

首先我是通过随机数生成一个四位数的随机数,用来匹配奖品的中奖概率,如果生成的随机数小于所设置的奖品中奖概率则会中奖,否则就不中奖,如果在抽奖过程中触发了上面的某一个拦截器,那么就也是谢谢参与的了。

当然,如果你真的一个不小心抽中了我们的超级大奖,你就会被拉入黑名单,之后的抽奖里面不论你怎么抽,都会是一个 谢谢参与 送给您。

4b95d66bb7f74884bb57519ea75bf06d.gif

正式开始

抽奖奖品的设计

首先因为这个抽奖是在特定的时间点限时开始的,所以为了可以让我们的会员大老爷们可以有个更好的体验,这里我是在创建活动的抽奖商品的时候,同时把抽奖商品的信息缓存到redis里面,因为抽奖商品总的就只有四个,所以可以直接把它们打包成一个数组,然后将这个数组转换成字符串的形式存储起来,之后需要的时候,再把他重新转换回来就可以了。

1
2
3
4
5
javascript复制代码if (!data || data.length <= 0) {
return
}
const list = JSON.stringify(data) //序列化
await ctx.redis.set('allList', list)

同时这样设计的一个好处就是,当奖品的一些数据发生改变的时候,直接删除这个key就完事了,因为对于redis来说,去更改数据相当于删除在重新创建数据,开销上会大很多。

接下来,在抽奖的过程中,用户会先去读取缓存上面的奖品,如果缓存上面没有的话,就会去数据库里面找,然后把找到的数据再给它存到redis里面去。

注意一个小细节

因为抽奖的概率都是随机的,虽然说大奖的中奖概率非常的低,但也会有可能说在某一个时间节点上,突然很多人中了大奖,然后把大奖一次性给抽空了,所以为了避免这种情况的出现,我们需要有一个在抽奖时间内的一个发奖计划。

例如,在一天可以分成24小时,奖品分成很多个等分,在热点时间,像是晚上八点这样的时间就发出去多点,具体实现我是这样的

先设置一份记录

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
javascript复制代码const houseData = [
// 中奖概率分成一百份,24小时每个时间段的机会
//100 / (3*24) = 28
//剩下28份分给不同的时间段
0,0,0,
1,1,1,
2,2,2,
3,3,3,
4,4,4,
5,5,5,
6,6,6,
7,7,7,
8,8,8,
9,9,9,
10,10,10,10,10,10,
11,11,11,11,11,
12,12,12,
13,13,13,
14,14,14,14,14,
15,15,15,15,15,15,
16,16,16,16,16,16,
17,17,17,
18,18,18,
19,19,19,19,19,19,
20,20,20,20,20,20,20,
21,21,21,21,21,21,21,
22,22,22,22,22,22,22,
23,23,23,
]

然后就开始进行发奖计划的重置

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
javascript复制代码 // 重置发奖计划
async resetPrizeData(data) {
const { prize_num, left_num, prize_time, time_begin } = data.dataValues
data.dataValues.prize_data = JSON.stringify(this.newHash)
if (prize_num < 0 || left_num < 0) {
return
}
if (prize_time <= 0) {
return
}
const num = Math.floor(prize_num / prize_time)

//每一天的发奖计划重制
let time = dayjs(time_begin)
if (num >= 1) {
for (let index = 1; index <= prize_time; index++) {
this.hash[time.format('YYYY-MM-DD')] = num
time = time.add(1, 'day')
}
const remainder = prize_num % prize_time
if (remainder) {
for (let index = 0; index < remainder; index++) {
const ran = Math.floor(Math.random() * (1, prize_time)) + 1
let t = dayjs(time_begin)
t = t.add(ran, 'day')
this.hash[t.format('YYYY-MM-DD')] += 1
}
}
for (const it in this.hash) {
this.setTime(it)
}
}
data.dataValues.prize_data = JSON.stringify(this.newHash)
}
//一天24小时的发奖计划
setTime(it) {
while (this.hash[it]) {
const day = dayjs(it)
this.hash[it]--
const ran = Math.floor(Math.random() * (0, 99))
const d = day.hour(houseData[ran])
const item = d.format('YYYY-MM-DD HH:mm:ss')
if (!this.newHash[item]) {
this.newHash[item] = 0
}
this.newHash[item] += 1
}
}

这样就可以避免出现突然在某一个时段把奖品给抽空了,造成后续的参与者没办法获得奖品的情况出现了。

用户抽奖次数,ip抽奖次数的限制,用户黑名单和ip黑名单

这里是为了防止某些人恶意刷抽奖的情况出现,如果一个用户已经达到了一定的抽奖次数,那么也就不允许这个用户再抽奖了,那么ip黑名单则是为了避免某一个ip地址下,通过申请不同的用户来参与抽奖。

同时ip黑名单和用户黑名单的作用上面也就提到了,如果是如果这个用户已经中奖了,则同时把这个用户和这个用户下的ip都一起拉黑,之后的抽奖过程中,如果他还继续抽奖,则给个虚拟币或者是谢谢参与给他。

开始抽奖

这里我的逻辑非常的简单,就是生成一个随机数,然后去匹配中奖区间,中了哪个就是哪个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
javascript复制代码const num = Math.floor(Math.random() * 10000) //生成抽奖编码
let prizeList = await prizeService.getData(ctx)
if (!prizeList) {
prizeList = await prizeService.setRData(ctx)
}
let it
for (let index = 0; index < prizeList.length; index++) {
if (
prizeList &&
prizeList[index] &&
prizeList[index].prize_code > num &&
!ctx.bool
) {
it = prizeList[index]
break
}
}
if (!it) {
throw new successExpection('没有中奖,谢谢参与')
}
ctx.it = it
await next()

这里所有的数据,我都是通过ctx的上下文,然后根据koa2的洋葱模型的方式,传递给下一个中间件。

发奖

中了奖,那么就需要有一个发奖的过程,这里我是设置了一个奖品池,用户从奖品池里面去抽取奖品,也就是把所有的奖品的个数,放到奖品池里面去,然后把奖品数通过redis缓存起来,这样在发奖的时候,就不用直接的去操作数据库,而是可以通过使用redis中的Incrby命令来完成相关的操作,当完成了相关的数据扣款之后,在通过异步任务的方式,来对数据库进行数据的同步,从而保持数据的一致性。

当然如果抽中了奖品,但是该奖品已经没有库存了,那么也是属于没有中奖。

大概的实现思路是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码  const item = await ctx.redis.hget('Pool', ctx.it.id)
if (item <= 0) {
throw new successExpection('没有中奖,谢谢参与')
}
const data = await ctx.redis.hincrby('Pool', ctx.it.id, -1)
if (data < 0) {
throw new successExpection('没有中奖,谢谢参与')
}
const prize = await Prize.findOne({
where: {
gType: num,
},
})
await prize.decrement(['left_num'])

还有一点需要注意的,就是在往奖品池里面放入奖品的时候,最好是加一个分布式锁,从而来控制只有一个服务去初始化我们的库存数据。

最后

实际开发工作中,类似的业务,肯定还会有更多的考量,本菜🐔只是写了一个有点类似于demo级别的东西,提供了一个思路,如果有哪里写的不好的地方,欢迎各位老哥执正👏。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

最新版Swagger 3升级指南和新功能体验!

发表于 2021-03-15

Swagger 3.0 发布已经有一段时间了,它于 2020.7 月 发布,但目前市面上使用的主流版本还是 Swagger 2.X 版本和少量的 1.X 版本,然而作为一名合格的程序员怎么能不折腾新技术呢?所以本期就大家带来一篇最新版 Swagger 的内容,本文会带大家看最新版 Swagger 有哪些改变?又是如何将老版本 Swagger 升级到新版的?

Swagger 是什么?

Swagger 是一个用于生成、描述和调用 RESTful 接口的 Web 服务。通俗的来讲,Swagger 就是将项目中所有(想要暴露的)接口展现在页面上,并且可以进行接口调用和测试的服务。

PS:Swagger 遵循了 OpenAPI 规范,OpenAPI 是 Linux 基金会的一个项目,试图通过定义一种用来描述 API 格式或 API 定义的语言,来规范 RESTful 服务开发过程。

Swagger 官网地址:swagger.io/

Swagger 有什么用?

从上述 Swagger 定义我们不难看出 Swagger 有以下 3 个重要的作用:

  1. 将项目中所有的接口展现在页面上,这样后端程序员就不需要专门为前端使用者编写专门的接口文档;
  2. 当接口更新之后,只需要修改代码中的 Swagger 描述就可以实时生成新的接口文档了,从而规避了接口文档老旧不能使用的问题;
  3. 通过 Swagger 页面,我们可以直接进行接口调用,降低了项目开发阶段的调试成本。

image.png

Swagger 旧版本使用

Swagger 旧版本也就是目前市面上主流的 V2 版本是 Swagger 2.9.2,在讲新版本之前,我们先来回顾一下 Swagger 2.9.2 是如何使用的。

Swagger 2.9.2 的使用分为以下 4 步:

  1. 添加依赖
  2. 开启 Swagger 功能
  3. 配置 Swagger 文档摘要信息
  4. 调用接口访问

下面我们分别来看。

1.添加依赖

首先,我们要去 mvnrepository 查询 Swagger 的依赖,搜索“springfox”关键字,得到结果的前两条依赖信息,就是我们想要的结果,如下图所示:
image.png
将这两个依赖添加带项目中:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>

<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

为什么是“springfox”?

问:我们要使用的是 Swagger,为什么要搜索“springfox”?

答:Swagger 可以看作是一个遵循了 OpenAPI 规范的一项技术,而 springfox 则是这项技术的具体实现。 就好比 Spring 中的 AOP 和 DI 一样,前者是思想,而后者是实现。

2.开启Swagger

在 Spring Boot 的启动类或配置类中添加 @EnableSwagger2 注释,开启 Swagger,部分核心代码如下:

1
2
3
java复制代码@EnableSwagger2
@SpringBootApplication
public class Application {...

3.配置摘要信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2) // 1.SWAGGER_2
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swaggerv2.controller")) // 2.设置扫描路径
.build();
}
}

4.访问Swagger

项目正常启动之后使用“http://localhost:8080/swagger-ui.html”访问Swagger页面,如下图所示:
image.png

Swagger 最新版使用

Swagger 最新版的配置步骤和旧版本是一样,只是每个具体的配置项又略有不同,具体步骤如下。

1.添加依赖

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/io.springfox/springfox-boot-starter -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>3.0.0</version>
</dependency>

从上述配置可以看出,Swagger 新版本的依赖项只有一个,而旧版本的依赖项有两个,相比来说也简洁了很多。

2.开启Swagger

在 Spring Boot 的启动类或配置类中添加 @EnableOpenApi 注释,开启 Swagger,部分核心代码如下:

1
2
3
java复制代码@EnableOpenApi
@SpringBootApplication
public class Application {...

3.配置摘要信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

@Configuration
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.OAS_30) // v2 不同
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.swaggerv3.controller")) // 设置扫描路径
.build();
}
}

从上述代码可以看出 Docket 的配置中只有文档的类型设置新老版本是不同的,新版本的配置是 OAS_30 而旧版本的配置是 SWAGGER_2。

PS:OAS 是 OpenAPI Specification 的简称,翻译成中文就是 OpenAPI 说明书。

4.访问Swagger

新版本的 Swagger 访问地址和老版本的地址是不同的,新版版的访问地址是“localhost:8080/swagger-ui/””,如下图所示:
image.png

新版本 VS 老版本

新版本和老版本的区别主要体现在以下 4 个方面:

  1. 依赖项的添加不同:新版本只需要添加一项,而老版本需要添加两项;
  2. 启动 Swagger 的注解不同:新版本使用的是 @EnableOpenApi,而老版本是 @EnableSwagger2;
  3. Docket(文档摘要信息)的文件类型配置不同:新版本配置的是 OAS_3,而老版本是 SWAGGER_2;
  4. Swagger UI 访问地址不同:新版本访问地址是“http://localhost:8080/swagger-ui/”,而老版本访问地址是“http://localhost:8080/swagger-ui.html”。

总结

Swagger 新版本让人印象深刻的优点有两个:第一,配置变得简单了,比如依赖项配置减少了 50%,第二,新版 Swagger 页面设计风格有了不小的改变,新版的页面让人感觉更加现代化也更加具有科技感了,总体来说美观了不少。

值得一提的是 Swagger 的整个升级过程很平滑,从老版本升级到新版本,只需要简单的配置即可,那些用于描述接口的注解还是延续了老版本的用法,这样就可以在不修改大部分主要代码的情况下,可以成功到最新版本啦。

关注公号「Java中文社群」查看 Swagger 3 视频版内容。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

求之不得的 Java 文档教程大汇总!

发表于 2021-03-15

已收录至免费编程资源大全:github.com/liyupi/free…

大家好,我是鱼皮,今天分享几个 GitHub 上非常实用的 Java 文档教程项目,包括 Java 基础知识、类库框架、系统设计、源码分析等。

用文档学习的好处是可以把控自己的学习节奏,并且更方便地记录笔记。习惯通过阅读文档来学习的朋友一定不要错过下面这些项目。

基础

On Java 8

《On Java 8》的中文版,是 Java8(主流 Java 版本)的在线学习手册,支持在文档中搜索内容,适合新手入门 Java。

原本是个开源项目,但最近由于出版了纸质书籍,目前项目应该不再维护了。

JavaGuide

全网知名的 Java 学习 + 面试指南,一份涵盖了大部分 Java 程序员所需要掌握的核心知识的电子文档。

该文档不仅对 Java 基础知识有全面的讲解,还包含了 Java 后端工程师必备技能的学习,比如 MySQL、Redis、系统设计等。不仅是一份教程,更是一份完整的 Java 相关技术栈知识点总结!

LearningNotes

这是一位 Java 学习者的笔记,包含 Java 基础、框架、Android 开发、设计模式、数据结构与算法、网络等知识体系,甚至还有自己的面试经历,非常全面!

建议所有学习编程的同学都学习一下该项目,平时多将自己学到的知识记录和整理。

java-learning

一份 Java 学习笔记,包含博客讲解和源码实例,包括 Java SE 和 Java Web 等知识点。

笔记内容主要是对一些基础特性和编程细节进行总结整理,适合了解 Java 基础语法,想对已学知识点进行巩固,并进一步深入学习的同学。

类库框架

SpringBoot Guide

JavaGuide 的作者 Guide 哥的又一个知名指南项目,专注于 SpringBoot 的教程和知识总结,还整理了一些实战项目帮助大家练手。帮助大家从 0 到 1 学习 SpringBoot,加强对 Spring 框架的理解。

springcloud-learning

引用作者对项目的介绍:这是一套涵盖大部分核心组件的 Spring Cloud 教程,包括 Spring Cloud Alibaba 及分布式事务 Seata,基于Spring Cloud Greenwich 及 SpringBoot 2.1.7。22篇文章,篇篇精华,32个 Demo,涵盖大部分应用场景。

扫了一遍目录,几乎把所有 Spring Cloud 的知识点和应用场景都讲了,非常适合已经能够开发单体应用,想要学习分布式、微服务开发的同学进阶。

spring-security-jwt-guide

JavaGuide 的作者 Guide 哥的又一个项目,以文档 + 源码的方式,带你从零入门 Spring Security 模块!

项目结构

guava-study

Guava 是 Google 的知名开源类库,包含了很多高质量的 API,可以使你的 Java 代码更加优雅,更加简洁。

本项目是对 Java 知名类库 guava 的学习,帮助你使用好 guava,让你工作更加高效愉悦。

RxJavaLearningMaterial

RxJava 本质上是一个异步操作库,是一个能让你用极其简洁的逻辑去处理繁琐复杂任务的异步事件库,深受广大 Android 开发者的喜爱。

该项目是一份详细的 RxJava 学习攻略 & 指南,从入门、原理到实战,都讲解的十分透彻,帮助你上手 RxJava。

系统设计

mall-learning

一套电商系统实战学习教程,包含架构、业务、技术要点的全方位解析。该电商系统使用现阶段主流技术实现。涵盖了 SpringBoot、MyBatis、Elasticsearch、RabbitMQ、Redis、MongoDB、Mysql 等技术,采用 Docker 容器化部署。

该项目无论是技术的广度还是深度都很卓越,如果能跟着作者的思路完整学习并自己实现,一定能掌握大多数 Java 后端开发知识点!

miaosha

秒杀系统一直是开发领域的难点。本项目是对秒杀系统的设计与实现,帮助你学习秒杀系统中那些关键的设计,开拓思维,成长为一名优秀的互联网工程师。

源码

JavaSourceCodeLearning

Java 流行框架的源码分析项目。目前包含 Spring 源码、SpringBoot 源码、SpringAOP 源码、SpringSecurity 源码、SpringSecurity OAuth2 源码、JDK 源码、Dubbo 源码等。讲解非常深入透彻,能够帮助大家更好地通过学习框架底层源码来提升自身的水平,在大厂面试时在众多面试者中脱颖而出!

LearningJDK

专注于 JDK 源码的阅读笔记,已经阅读了几百个 JDK 类,适合同样想深入了解 JDK 的同学阅读学习。


以上就是本期分享,希望对大家有帮助~

其实 GitHub 上还有非常多优秀的 Java 学习项目,但我并没有把它们归类为教程,而是整理到了其他类目中,在后面会推荐给大家!

所有资源都在 项目汇总 / 教程:www.code-nav.cn/free

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…705706707…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%