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

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


  • 首页

  • 归档

  • 搜索

最大公约数和最小公倍数

发表于 2021-10-25

小知识,大挑战!本文正在参与「程序员必备小知识」创作活动

本文已参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

/***********************************************************************

目的:求输入两个数的最大公约数

分析:确定较大值或较小值,并把较小值拷贝。
最大公约数不会超过两个数的较小值,所以从较小值开始往下找,直到能同时被两个数整除

24 18

24 17

…

25 6

平台:Visual studio 2017 && windows
*************************************************************************/

📝 实现代码1:

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
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
//存储最大公约数
int gcd = 0;
//确定最大值为m
if (m < n)
{
int temp = m;
m = n;
n = temp;
}
//假设最大公约数是n
gcd = n;
while (1)
{
if (m % gcd == 0 && n % gcd == 0)
{
printf("%d\n", gcd);
break;
}
gcd--;
}
}

/***********************************************************************

目的:优化代码1:

分析:可以不用确定较大值或较小值,但是必须得拷贝一份n或m

18 24

18 23

…

18 6

平台:Visual studio 2017 && windows
*************************************************************************/

📝 优化代码1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
int gcd = 0;
//假设最大公约数是m
gcd = m;
while (1)
{
if (m % gcd == 0 && n % gcd == 0)
{
printf("%d\n", gcd);
break;
}
gcd--;
}
}

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

/***********************************************************************

目的:欧几里德算法 求最大公约数

算法简介:欧几里得算法又称辗转相除法,古希腊数学家欧几里得在其著作《The Elements》中最早描述了这种算法,所以被命名为欧几里得算法。扩展欧几里得算法可用于RSA加密等领域。

在这里插入图片描述

算法原理:通俗来讲就是以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数

24   18

24 % 18 = 1 … 6

18 % 6 = 3 … 0

平台:Visual studio 2017 && windows
*************************************************************************/

📝 实现代码2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
while(m % n)
{
//交换被除数和除数
int temp = m % n;
m = n;
n = temp;
}
printf("%d\n", n);
return 0;
}

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

/***********************************************************************

目的:更相减损术 求最大公约数

算法简介:《九章算术》是中国古代的数学专著,其中的“更相减损术”可以用来求两个数的最大公约数,它原本是为约分而设计的,但它适用于任何需要求最大公约数的场合。

image.png

算法原理:

▶ 第一步:任意给定两个正整数;判断它们是否都是偶数。若是,则用2约简;若不是则执行第二步。

▶ 第二步:以较大的数减较小的数,接着把所得的差与较小的数比较,并以大数减小数。继续这个操作,直到所得的减数和差相等为止。

▶ 则第一步中约掉的若干个2的积与第二步中等数的乘积就是所求的最大公约数。
其中所说的“等数”,就是公约数。求“等数”的办法是“更相减损”法。

在这里插入图片描述

平台:Visual studio 2017 && windows

*************************************************************************/

📝 实现代码2:

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
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
int flag = 0;//为了区分这两个数有没有约简2
int count = 0;//记录约简了几次2
int temp = 0;
while(1)
{
//两个数相同
if(m == n)
{
flag = -1;
break;
}
//全为偶
if(m % 2 == 0 && n % 2 == 0)
{
flag = 1;
count++;
m /= 2;
n /= 2;
}
//不全为偶
else
{
//大减小
if(m > n)
{
temp = m - n;
}
else
{
temp = n - m;
}
//比较减数和差
if(n > temp)
{
m = n;
n = temp;
}
else if(n < temp)
{
m = temp;
}
else
{
//减数和差相等
break;
}
}
}
//约简2
if(1 == flag)
{
printf("%d\n", 2 * count * n);
}
//两数相等
else if(-1 == flag)
{
printf("%d\n", m);
}
else
{
printf("%d\n", n);
}
return 0;
}

/***********************************************************************

目的:优化 更相减损术 求最大公约数

算法原理:可将上面的步骤理解为:

▶ 若m > n,则m = m - n

▶ 若m < n,则 n = n - m

▶ 若m = n,则最大公约数为m

平台:Visual studio 2017 && windows

*************************************************************************/

📝 优化代码2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
while (m != n)
{
if (m > n)
m -= n;
else
n -= m;
}
printf("%d\n", m);
return 0;
}

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++

比较:更相减损术和辗转相除法的主要区别在于前者所使用的运算是“减”,后者是“除”。从算法思想上看,两者并没有本质上的区别,但是在计算过程中,如果遇到一个数很大,另一个数比较小的情况,可能要进行很多次减法才能达到一次除法的效果,从而使得算法的时间复杂度退化为O(N),其中N是原先的两个数中较大的一个。相比之下,辗转相除法的时间复杂度稳定于O(logN)。

这场来自中西方的磨擦就此打住

image.png

当然还有其它的一些算法

穷举法 :一听就很暴力

Stein算法 :针对欧几里德算法在对大整数进行运算时,需要试商导致增加运算时间的缺陷而提出的改进算法

image.png


/***********************************************************************

目的:求输入两个数的最小公倍数

算法原理:

▶ 利用求出最大公约数,用两个数的乘积除以最大公约数即可。

▶ 最小公倍数不会小于两个数的较大值,所以从较大值开始往上找

平台:Visual studio 2017 && windows

*************************************************************************/

📝 实现代码3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
//拷贝一份m和n
int tmp_m = m;
int tmp_n = n;
while (m != n)
{
if (m > n)
m -= n;
else
n -= m;
}
printf("%d\n", tmp_m * tmp_n / m);
return 0;
}

📝 实现代码4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
c复制代码#include<stdio.h>
#pragma warning(disable:4996)
int main()
{
int m = 0;
int n = 0;
scanf("%d%d", &m, &n);
int temp = m;
while(1)
{
if(temp % m == 0 && temp % n == 0)
{
printf("%d\n", temp);
break;
}
temp++;
}
return 0;
}

本文转载自: 掘金

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

Java程序员必须知道的Java10特性

发表于 2021-10-25

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

在上一篇我们对Java 9的特性进行了一些回顾,今天接着来看看Java 10带来了什么特性。之所以需要把Java 8 到Java 17的特性归纳一遍,因为Java社区对Java 17的重视程度前所未有。话不多说,让我们走进Java 10。

Java 10

从Java 10 开始,Java的迭代周期缩短为半年,半年发布一个版本。

局部变量类型推断

在Java 6时初始化一个Map需要我们这样来声明:

1
arduino复制代码 Map<String, String> map = new HashMap<String,String>();

事实上泛型方法的参数可以通过上下文推导出来,所以在Java 7 中简化为:

1
arduino复制代码 Map<String, String> map = new HashMap<>();

到了Java 10 进一步升华了类型推断,我们看一个例子:

1
2
ini复制代码         var map = Map.of("hello","world");
         String var = map.get("hello");

猛一看还以为是Javascript的写法,事实上这就是Java。编译器从右侧的初始化程序的类型推断出初始化类型,这将大量减少一些样板代码。不过请注意,此特性仅适用于初始化局部变量,它不能用于成员变量、方法参数、返回类型等场景中。

另一件要注意的事情是var 并不是Java中的关键字,这确保了Java的向后兼容性。另外使用var没有运行时开销,也不会使 Java 成为动态语言。var标记的变量的类型仍然是在编译时推断出来。

var 不应该被滥用

虽然这样“爽起来了”,但是var也不应该被滥用。

下面这种写法明细可读性差,导致变量的类型需要你去DEBUG:

1
ini复制代码 var data = someObject.getData();

流中也尽量不要使用:

1
2
3
4
css复制代码 // 可读性差
 var names= apples.stream()
    .map(Apple::getName)
    .collect(Collectors.toList());

因此,在使用var时应该保证必要的可读性。

另外,在多态这个重要的Java特性中,var表现的并不是很完美。如果Fruit有Apple和Orange两种实现。

1
ini复制代码 var x = new Apple();

如果我们对x重新赋值为new Orange()就会报错,因为编译后x的类型就已经固定下来了。所以var和泛型一样都是在编译过程中起了作用。你必须保证var的类型是确定的。

那么话又说回来了,var结合泛型的钻石符号<>会有什么情况发生呢?

下面的 empList的类型是ArrayList<Object>:

1
ini复制代码 var empList = new ArrayList<>();

如果我们需要明确集合中放的都是Apple就必须在右边显式声明:

1
ini复制代码 var apples = new ArrayList<Apple>();

不可变集合

其实在Java 9中不可变集合已经得到了一些加强,在Java 10中进一步加强了不可变集合。为什么不可变集合变得如此重要?

  • 不可变性(immutability),这是函数式编程的基石之一,因此加强不可变集合有助于函数式编程在Java中的发展。
  • 安全性,由于集合不可变,因此就不存在竞态条件,天然的线程安全性,无论在编码过程中和内存使用中都有一定的优势,这种特性在Scala和Kotlin这两种编程语言中大放异彩。

在Java 10 中又引入了一些新的API。

集合副本

复制一个集合为不可变集合:

1
ini复制代码  List<Apple> copyList = List.copyOf(apples);

任何修改此类集合的尝试都会导致java.lang.UnsupportedOperationException异常。

Stream归纳为不可变集合

之前Stream API的归纳操作collect(Collector collector)都只会把流归纳为可变集合,现在它们都有对应的不可变集合了。举个例子:

1
2
3
css复制代码 List<String> names= apples.stream()
    .map(Apple::getName)
    .collect(Collectors.toUnmodifiableList());

Optional.orElseThrow()

1
2
3
vbnet复制代码         Optional<String> optional = Optional.ofNullable(nullableVal);
         // 可能会 NoSuchElementException
         String  nullable = optional.get();

Optional如果值为null时去get会抛出NoSuchElementException异常。从语义上get应该肯定能得到什么东西,但是实际上异常了,这种歧义性太大了。所以增加了一个orElseThrow()方法来增强语义性。

其它增强特性

Java 10的性能也明显加强了,支持G1并行垃圾收集。另外引入了即时编译技术(JIT),该技术可以加速java程序的运行速度。 另外Java 10对容器集成也进行了优化,JVM会根据容器的配置进行选择CPU核心数量和内存占用。还有其它一些底层优化特性这里就不多说了,了解为主,当你达到一定的层次会自己去了解的。到此Java 10的一些变化就归纳完了,其实并不是很多,都很好掌握。多多关注,不要走开,下次我们将对Java 11的一些变化和改进进行归纳。

本文转载自: 掘金

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

填个坑!再谈线程池动态调整那点事。

发表于 2021-10-25

本文已参与「掘力星计划」赢取创作大礼包,挑战创作激励金。

你好呀,我是歪歪。

前几天和一个大佬聊天的时候他说自己最近在做线程池的监控,刚刚把动态调整的功能开发完成。

想起我之前写过这方面的文章,就找出来看了一下:《如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。》

然后给我指出了一个问题,我仔细思考了一下,好像确实是留了一个坑。

为了更好的描述这个坑,我先给大家回顾一下线程池动态调整的几个关键点。

首先,为什么需要对线程池的参数进行动态调整呢?

因为随着业务的发展,有可能出现一个线程池开始够用,但是渐渐的被塞满的情况。

这样就会导致后续提交过来的任务被拒绝。

没有一劳永逸的配置方案,相关的参数应该是随着系统的浮动而浮动的。

所以,我们可以对线程池进行多维度的监控,比如其中的一个维度就是队列使用度的监控。

当队列使用度超过 80% 的时候就发送预警短信,提醒相应的负责人提高警惕,可以到对应的管理后台页面进行线程池参数的调整,防止出现任务被拒绝的情况。

以后有人问你线程池的各个参数怎么配置的时候,你先把分为 IO 密集型和 CPU 密集型的这个八股文答案背完之后。

加上一个:但是,除了这些方案外,我在实际解决问题的时候用的是另外一套方案”。

然后把上面的话复述一遍。

那么线程池可以修改的参数有哪些呢?

正常来说是可以调整核心线程数和最大线程数的。

线程池也直接提供了其对应的 set 方法:

但是其实还有一个关键参数也是需要调整的,那就是队列的长度。

哦,对了,说明一下,本文默认使用的队列是 LinkedBlockingQueue。

其容量是 final 修饰的,也就是说指定之后就不能修改:

所以队列的长度调整起来稍微要动点脑筋。

至于怎么绕过 final 这个限制,等下就说,先先给大家上个代码。

我一般是不会贴大段的代码的,但是这次为什么贴了呢?

因为我发现我之前的那篇文章就没有贴,之前写的代码也早就不知道去哪里了。

所以,我又苦哈哈的敲了一遍…

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
arduino复制代码import cn.hutool.core.thread.NamedThreadFactory;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadChangeDemo {

    public static void main(String[] args) {
        dynamicModifyExecutor();
    }

    private static ThreadPoolExecutor buildThreadPoolExecutor() {
        return new ThreadPoolExecutor(2,
                5,
                60,
                TimeUnit.SECONDS,
                new ResizeableCapacityLinkedBlockingQueue<>(10),
                new NamedThreadFactory("why技术", false));
    }

    private static void dynamicModifyExecutor() {
        ThreadPoolExecutor executor = buildThreadPoolExecutor();
        for (int i = 0; i < 15; i++) {
            executor.execute(() -> {
                threadPoolStatus(executor,"创建任务");
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        threadPoolStatus(executor,"改变之前");
        executor.setCorePoolSize(10);
        executor.setMaximumPoolSize(10);
        ResizeableCapacityLinkedBlockingQueue<Runnable> queue = (ResizeableCapacityLinkedBlockingQueue)executor.getQueue();
        queue.setCapacity(100);
        threadPoolStatus(executor,"改变之后");
    }

    /**
     * 打印线程池状态
     *
     * @param executor
     * @param name
     */
    private static void threadPoolStatus(ThreadPoolExecutor executor, String name) {
        BlockingQueue<Runnable> queue = executor.getQueue();
        System.out.println(Thread.currentThread().getName() + "-" + name + "-:" +
                "核心线程数:" + executor.getCorePoolSize() +
                " 活动线程数:" + executor.getActiveCount() +
                " 最大线程数:" + executor.getMaximumPoolSize() +
                " 线程池活跃度:" +
                divide(executor.getActiveCount(), executor.getMaximumPoolSize()) +
                " 任务完成数:" + executor.getCompletedTaskCount() +
                " 队列大小:" + (queue.size() + queue.remainingCapacity()) +
                " 当前排队线程数:" + queue.size() +
                " 队列剩余大小:" + queue.remainingCapacity() +
                " 队列使用度:" + divide(queue.size(), queue.size() + queue.remainingCapacity()));
    }

    private static String divide(int num1, int num2) {
        return String.format("%1.2f%%", Double.parseDouble(num1 + "") / Double.parseDouble(num2 + "") * 100);
    }
}

当你把这个代码粘过去之后,你会发现你没有 NamedThreadFactory 这个类。

没有关系,我用的是 hutool 工具包里面的,你要是没有,可以自定义一个,也可以在构造函数里面不传,这不是重点,问题不大。

问题大的是 ResizeableCapacityLinkedBlockingQueue 这个玩意。

它是怎么来的呢?

在之前的文章里面提到过:

就是把 LinkedBlockingQueue 粘贴一份出来,修改个名字,然后把 Capacity 参数的 final 修饰符去掉,并提供其对应的 get/set 方法。

感觉非常的简单,就能实现 capacity 参数的动态变更。

但是,我当时写的时候就感觉是有坑的。

毕竟这么简单的话,为什么官方要把它给设计为 final 呢?

坑在哪里?

关于 LinkedBlockingQueue 的工作原理就不在这里说了,都是属于必背八股文的内容。

主要说一下前面提到的场景中,如果我直接把 final 修饰符去掉,并提供其对应的 get/set 方法,这样的做法坑在哪里。

先说一下,如果没有特殊说明,本文中的源码都是 JDK 8 版本。

我们看一下这个 put 方法:

主要看这个被框起来的部分。

while 条件里面的 capacity 我们知道代表的是当前容量。

那么 count.get 是个什么玩意呢?

就是当前队列里面有多少个元素。

count.get == capacity 就是说队列已经满了,然后执行 notFull.await() 把当前的这个 put 操作挂起来。

来个简单的例子验证一下:

申请一个长度为 5 的队列,然后在循环里面调用 put 方法,当队列满了之后,程序就阻塞住了。

通过 dump 当前线程可以知道主线程确实是阻塞在了我们前面分析的地方:

所以,你想想。如果我把队列的 capacity 修改为了另外的值,这地方会感知到吗?

它感知不到啊,它在等着别人唤醒呢。

现在我们把队列换成我修改后的队列验证一下。

下面验证程序的思路就是在一个子线程中执行队列的 put 操作,直到容量满了,被阻塞。

然后主线程把容量修改为 100。

上面的程序其实我想要达到的效果是当容量扩大之后,子线程不应该继续阻塞。

但是经过前面的分析,我们知道这里并不会去唤醒子线程。

所以,输出结果是这样的:

子线程还是阻塞着,所以并没有达到预期。

所以这个时候我们应该怎么办呢?

当然是去主动唤醒一下啦。

也就是修改一下 setCapacity 的逻辑:

1
2
3
4
5
6
7
8
arduino复制代码public void setCapacity(int capacity) {
    final int oldCapacity = this.capacity;
    this.capacity = capacity;
    final int size = count.get();
    if (capacity > size && size >= oldCapacity) {
        signalNotFull();
    }
}

核心逻辑就是发现如果容量扩大了,那么就调用一下 signalNotFull 方法:

唤醒一下被 park 起来的线程。

如果看到这里你觉得你有点懵,不知道 LinkedBlockingQueue 的这几个玩意是干啥的:

赶紧去花一小时时间补充一下 LinkedBlockingQueue 相关的知识点。这样玩意,面试也经常考的。

好了,我们说回来。

修改完我们自定义的 setCapacity 方法后,再次执行程序,就出现了我们预期的输出:

除了改 setCapacity 方法之外,我在写文章的时候不经意间还触发了另外一个答案:

在调用完 setCapacity 方法之后,再次调用 put 方法,也能得到预期的输出:

我们观察 put 方法就能发现其实道理是一样的:

当调用完 setCapacity 方法之后,再次调用 put 方法,由于不满足标号为 ① 的代码的条件,所以就不会被阻塞。

于是可以顺利走到标号为 ② 的地方唤醒被阻塞的线程。

所以也就变相的达到了改变队列长度,唤醒被阻塞的任务目的。

而究根结底,就是需要执行一次唤醒的操作。

那么那一种优雅一点呢?

那肯定是第一种把逻辑封装在 setCapacity 方法里面操作起来更加优雅。

第二种方式,大多适用于那种“你也不知道为什么,反正这样写程序就是正常了”的情况。

现在我们知道在线程池里面动态调整队列长度的坑是什么了。

那就是队列满了之后,调用 put 方法的线程就会被阻塞住,即使此时另外的线程调用了 setCapacity 方法,改变了队列长度,如果没有线程再次触发 put 操作,被阻塞的线程也不会被唤醒。

是不是?

了不了解?

对不对?

这是不对的,朋友们。

看到前面内容,频频点头的朋友,要注意了。

这地方要开始转弯了。

开始转弯

线程池里面往队列里面添加对象的时候,用的是 offer 命令,并没有用 put 命令:

我们看看 offer 命令在干啥事儿:

队列满了之后,直接返回 false,不会出现阻塞的情况。

也就是说,线程池中根本就不会出现我前面说的需要唤醒的情况,因为根本就没有阻塞中的线程。

在和大佬交流的过程中,他提到了一个 VariableLinkedBlockingQueue 的东西。

这个类位于 MQ 包里面,我前面提到的 setCapacity 方法的修改方式就是在它这里学来的:

同时,项目里面也用到了它的 put 方法:

所以,它是有可能出现我们前面分析的情况,有需要被唤醒的线程。

但是,你想想,线程池里面并没有使用 put 方法,是不是就刚好避免这样的情况?

是的,确实是。

但是,不够严谨,如果知道有问题了的话,为什么要留个坑在这里呢?

你学 MQ 的 VariableLinkedBlockingQueue 考虑的周全一点,就算 put 方法阻塞的时候也能用,它不香吗?

写到这里其实好像除了让你熟悉一下 LinkedBlockingQueue 外,似乎是一个没啥卵用的知识点,

但是,我能让这个没有卵用的知识点起到大作用。

因为这其实是一个小细节。

假设我出去面试,在面试的时候提到动态调整方法的时候,在不经意间拿捏一下这个小细节,即使我没有真的落地过动态调整,但是我提到这样的一个小细节,就显得很真实。

面试官一听:很不错,有整体,有局部,应该是假不了。

在 VariableLinkedBlockingQueue 里面还有几处细节,拿 put 方法来说:

判断条件从 count.get() >= capacity 变成了 count.get() = capacity,目的是为了支持 capacity 由大变小的场景。

这样的地方还有好几处,就不一一列举了。

魔鬼,都在细节里面。

同学们得好好的拿捏一下。

JDK bug

其实原计划写到前面,就打算收尾了,因为我本来就只是想补充一下我之前没有注意到的细节。

但是,我手贱,跑到 JDK bug 列表里面去搜索了一下 LinkedBlockingQueue,想看看还有没有什么其他的收获。

我是万万没想到,确实是有一点意外收获的。

首先是这一个 bug ,它是在 2019-12-29 被提出来的:

bugs.openjdk.java.net/browse/JDK-…

看标题的意思也是想要给 LinkedBlockingQueue 赋能,可以让它的容量进行修改。

加上他下面的场景描述,应该也想要和线程池配合,找到队列的抓手,下钻到底层逻辑,联动监控系统,拉通配置页面,打出一套动态适应的组合拳。

但是官方并没有采纳这个建议。

回复里面说写 concurrent 包的这些哥们对于在并发类里面加东西是非常谨慎的。他们觉得给 ThreadPoolExecutor 提供可动态修改的特性会带来或者已经带来众多的 bug 了。

我理解就是简单一句话:建议还是不错的,但是我不敢动。并发这块,牵一发动全身,不知道会出些什么幺蛾子。

所以要实现这个功能,还是得自己想办法。

这里也就解释了为什么用 final 去修饰了队列的容量,毕竟把功能缩减一下,出现 bug 的几率也少了很多。

第二个 bug 就有意思了,和我们动态调整线程池的需求非常匹配:

bugs.openjdk.java.net/browse/JDK-…

这是一个 2020 年 3 月份提出的 bug,描述的是说在更新线程池的核心线程数的时候,会抛出一个拒绝异常。

在 bug 描述的那部分他贴了很多代码,但是他给的代码写的很复杂,不太好理解。

好在 Martin 大佬写了一个简化版,一目了然,就好理解的多:

这段代码是干了个啥事儿呢,简单给大家汇报一下。

首先 main 方法里面有个循环,循环里面是调用了 test 方法,当 test 方法抛出异常的时候循环结束。

然后 test 方法里面是每次都搞一个新的线程池,接着往线程池里面提交队列长度加最大线程数个任务,最后关闭这个线程池。

同时还有另外一个线程把线程池的核心线程数从 1 修改为 5。

你可以打开前面提到的 bug 链接,把这段代码贴出来跑一下,非常的匪夷所思。

Martin 大佬他也认为这是一个 BUG.

说实在的,我跑了一下案例,我觉得这应该算是一个 bug,但是经过 Doug Lea 老爷子的亲自认证,他并不觉得这是一个 Bug。

主要是这个 bug 确实也有点超出我的认知,而且在链接中并没有明确的说具体原因是什么,导致我定位的时间非常的长,甚至一度想要放弃。

但是最终定位到问题之后也是长叹一口:害,就这?没啥意思。

先看一下问题的表现是怎么样的:

上面的程序运行起来后,会抛出 RejectedExecutionException,也就是线程池拒绝执行该任务。

但是我们前面分析了,for 循环的次数是线程池刚好能容纳的任务数:

按理来说不应该有问题啊?

这也就是提问的哥们纳闷的地方:

他说:我很费解啊,我提交的任务数量根本就不会超过 queueCapacity+maxThreads,为什么线程池还抛出了一个 RejectedExecutionException?而且这个问题非常的难以调试,因为在任务中添加任何形式的延迟,这个问题都不会复现。

他的言外之意就是:这个问题非常的莫名其妙,但是我可以稳定复现,只是每次复现出现问题的时机都非常的随机,我搞不定了,我觉得是一个 bug,你们帮忙看看吧。

我先不说我定位到的 Bug 的主要原因是啥吧。

先看看老爷子是怎么说的:

老爷子的观点简单来说就是四个字:

老爷子说他没有说服自己上面的这段程序应该被正常运行成功。

意思就是他觉得抛出异常也是正常的事情。但是他没有说为什么。

一天之后,他又补了一句话:

我先给大家翻译一下:

他说当线程池的 submit 方法和 setCorePoolSize 或者 prestartAllCoreThreads 同时存在,且在不同的线程中运行的时候,它们之间会有竞争的关系。

在新线程处于预启动但还没完全就绪接受队列中的任务的时候,会有一个短暂的窗口。在这个窗口中队列还是处于满的状态。

解决方案其实也很简单,比如可以在 setCorePoolSize 方法中把预启动线程的逻辑拿掉,但是如果是用 prestartAllCoreThreads 方法,那么还是会出现前面的问题。

但是,不管是什么情况吧,我还是不确定这是一个需要被修复的问题。

怎么样,老爷子的话看起来是不是很懵?

是的,这段话我最开始的时候读了 10 遍,都是懵的,但是当我理解到这个问题出现的原因之后,我还是不得不感叹一句:

还是老爷子总结到位,没有一句废话。

到底啥原因?

首先我们看一下示例代码里面操作线程池的这两个地方:

修改核心线程数的是一个线程,即 CompletableFuture 的默认线程池 ForkJoinPool 中的一个线程。

往线程池里面提交任务是另外一个线程,即主线程。

老爷子的第一句话,说的就是这回事:

racing,就是开车,就是开快车,就是与…比赛的意思。

这是一个多线程的场景,主线程和 ForkJoinPool 中的线程正在 race,即可能出现谁先谁后的问题。

接着我们看看 setCorePoolSize 方法干了啥事:

标号为 ① 的地方是计算新设置的核心线程数与原核心线程数之间的差值。

得出的差值,在标号为 ② 的地方进行使用。

也就是取差值和当前队列中正在排队的任务数中小的那一个。

比如当前的核心线程数配置就是 2,这个时候我要把它修改为 5。队列里面有 10 个任务在排队。

那么差值就是 5-2=3,即标号为 ① 处的 delta=3。

workQueue.size 就是正在排队的那 10 个任务。

也就是 Math.min(3,10),所以标号为 ② 处的 k=3。

含义为需要新增 3 个核心线程数,去帮忙把排队的任务给处理一下。

但是,你想新增 3 个就一定是对的吗?

会不会在新增的过程中,队列中的任务已经被处理完了,有可能根本就不需要 3 个这么多了?

所以,循环终止的条件除了老老实实的循环 k 次外,还有什么?

就是队列为空的时候:

同时,你去看代码上面的那一大段注释,你就知道,其实它描述的和我是一回事。

好,我们接着看 addWorker 里面,我想要让你看到地方:

在这个方法里面经过一系列判断后,会走入到 new Worker() 的逻辑,即工作线程。

然后把这个线程加入到 workers 里面。

workers 就是一个存放工作线程的 HashSet 集合:

你看我框起来的这两局代码,从 workers.add(w) 到 t.start()。

从加入到集合到真正的启动,中间还有一些逻辑。

执行中间的逻辑的这一小段时间,就是老爷子说的 “window”。

there’s a window while new threads are in the process of being prestarted but not yet taking tasks。

就是在新线程处于预启动,但尚未接受任务时,会有一个窗口。

这个窗口会发生啥事儿呢?

就是下面这句话:

the queue may remain (transiently) full。

队列有可能还是满的,但是只是暂时的。

接下来我们连起来看:

所以怎么理解上面被划线的这句话呢?

带入一个实际的场景,也就是前面的示例代码,只是调整一下参数:

这个线程池核心线程数是 1,最大线程数是 2,队列长度是 5,最多能容纳的任务数是 7。

另外有一个线程在执行把核心线程池从 1 修改为 2 的操作。

假设我们记线程池 submit 提交了 6 个任务,正在提交第 7 个任务的时间点为 T1。

为什么是要强调这个时间点呢?

因为当提交第 7 个任务的时候,就需要去启用非核心线程数了。

具体的源码在这里:

java.util.concurrent.ThreadPoolExecutor#execute

也就是说此时队列满了, workQueue.offer(command) 返回的是 fasle。因此要走到 addWorker(command, false) 方法中去了。

代码走到 1378 行这个时间点,是 T1。

如果 1378 行的 addWorker 方法返回 false,说明添加工作线程失败,抛出拒绝异常。

前面示例程序抛出拒绝异常就是因为这里返回了 fasle。

那么问题就变成了:为什么 1378 行中的 addWorker 执行后返回了 false 呢?

因为当前不满足这个条件了 wc >= (core ? corePoolSize : maximumPoolSize):

wc 就是当前线程池,正在工作的线程数。

把我们前面的条件带进去,就是这样的 wc >=(false?2:2)。

即 wc=2。

为什么会等于 2,不应该是 1 吗?

多的哪一个是哪里来的呢?

真相只有一个:恰好此时 setCorePoolSize 方法中的 addWorker 也执行到了 workers.add(w),导致 wc 从 1 变成了 2。

撞车了,所以抛出拒绝异常。

那么为什么大多数情况下不会抛出异常呢?

因为从 workers.add(w) 到 t.start()这个时间窗口,非常的短暂。

大多数情况下,setCorePoolSize 方法中的 addWorker 执行了后,就会理解从队列里面拿一个任务出来执行。

而这个情况下,另外的任务通过线程池提交进来后,发现队列还有位子,就放到队列里面去了,根本不会去执行 addWorker 方法。

道理,就是这样一个道理。

这个多线程问题确实是比较难复现,我是怎么定位到的呢?

加日志。

源码里面怎么加日志呢?

我不仅搞了一个自定义队列,还把线程池的源码粘出来了一份,这样就可以加日志了:

另外,其实我这个定位方案也是很不严谨的。

调试多线程的时候,最好是不要使用 System.out.println,有坑!

场景

我们再回头看看老爷子给出的方案:

其实它给了两个。

第一个是拿掉 setCorePoolSize 方法中的 addworker 的逻辑。

第二个是说原程序中,即提问者给的程序中,使用的是 prestartAllCoreThreads 方法,这个里面必须要调用 addWorker 方法,所以还是有一定的几率出现前面的问题。

但是,老爷子不明白为什么会这样写?

我想也许他是没有想到什么合适的场景?

其实前面提到的这个 Bug,其实在动态调整的这个场景下,还是有可能会出现的。

虽然,出现的概率非常低,条件也非常苛刻。

但是,还是有几率出现的。

万一出现了,当同事都在抠脑壳的时候,你就说:这个嘛,我见过,是个 Bug。不一定每次都出现的。

这又是一个你可以拿捏的小细节。

但是,如果你在面试的时候遇到这个问题了,这属于一个傻逼问题。

毫无意义。

属于,面试官不知道在哪看到了一个感觉很厉害的观点,一定要展现出自己很厉害的样子。

但是他不知道的是,这个题:

最后说一句

好了,看到了这里了,安排一个点赞吧。写文章很累的,需要一点正反馈。

给各位读者朋友们磕一个了:

本文已收录自个人博客,欢迎大家来玩:

www.whywhy.vip/

本文转载自: 掘金

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

真卷,这掘友的java基础竟如此牢固 类加载器源码探究

发表于 2021-10-25

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

写在前面

在jvm系列:类加载机制一文中已经对ClassLoader做了介绍,本文将从源码角度继续对ClassLoader做更深入的探索。

ClassLoader是一个抽象的类加载器对象,负责去加载类。给定了一个类的“二进制名称”,一个类加载器需要尝试去定位或者生成一个数据,该数据构成了一个定义的类。“二进制名称”:任意一个类名被提供作为ClassLoader方法的字符串参数,这个字符串形式的类名字必须是一个二进制名称,且是由java语言规范定义的,如下:

1
2
3
php复制代码"java.lang.String"
"java.net.URLClassLoader$3$1"
"java.security.KeyStore$Builder$FileBuilder$1"

类的关系图如下:

25979403-cf2c2873ecb24ecc.webp

25979403-343a2abf84fde107.webp
launcher核心类:

25979403-25ecd4393abf8147.webp

Launcher类

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
java复制代码public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
//静态变量,初始化,会执行构造方法
private static Launcher launcher = newLauncher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}

//构造方法执行
public Launcher() {
Launcher.ExtClassLoader var1;
try {
//初始化扩展类加载器
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Couldnotcreateextension class loader", var10);
}
try {
//初始化应用类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置ContextClassLoader,设置为扩展类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager) this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager:" + var2);
}
System.setSecurityManager(var3);
}
}

这里构造方法Launcher()中做了四件事情:

创建扩展类加载器

创建应用程序类加载器

设置ContextClassLoader

如果需要安装安全管理器 security manager

其中launcher是static的,所以初始化的时候就会创建对象,也就是触发了构造方法,所以初始化的时候就会执行上面四个步骤。
再继续看ExtClassLoader的创建中的关键几步:

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
scss复制代码static class ExtClassLoader extends URLClassLoader {
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
final File[] var0 = getExtDirs();
try {
return (Launcher.ExtClassLoader) AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
public Launcher.ExtClassLoader run() throws IOException {
int var1 = var0.length;
for (int var2 = 0; var2 < var1; ++var2) {
MetaIndex.registerDirectory(var0[var2]);
}
return new Launcher.ExtClassLoader(var0);
}
});
} catch (PrivilegedActionException var2) {
throw (IOException) var2.getException();
}
}

void addExtURL(URL var1) {
super.addURL(var1);
}

public ExtClassLoader(File[] var1) throws IOException {
super(getExtURLs(var1), (ClassLoader) null, Launcher.factory);
SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

private static File[] getExtDirs() {
String var0 = System.getProperty("java.ext.dirs");
File[] var1;
if (var0 != null) {
StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
int var3 = var2.countTokens();
var1 = new File[var3];
for (int var4 = 0; var4 < var3; ++var4) {
var1[var4] = new File(var2.nextToken());
}
} else {
var1 = new File[0];
}
return var1;
}
}

还有AppClassLoader的创建中的关键几步:

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
java复制代码    //var1 类全名 * var2 是否连接该类
public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {
int var3 = var1.lastIndexOf(46);
if (var3 != -1) {
SecurityManager var4 = System.getSecurityManager();
if (var4 != null) {
var4.checkPackageAccess(var1.substring(0, var3));
}
}
//通常是false,想要返回TRUE可能需要设置启动参数 lookupCacheEnabled 为true。
//为true时,具体的逻辑是C++写的
if (this.ucp.knownToNotExist(var1)) {
//如果这个类已经被这个类加载器加载,则返回这个 类,否则返回Null
Class var5 = this.findLoadedClass(var1);
if (var5 != null) {
if (var2) {
//如果该类没有被连接,则连接,否则什么都不做
this.resolveClass(var5);
}
return var5;
} else {
throw new ClassNotFoundException(var1);
}
} else {
return super.loadClass(var1, var2);
}
}

ClassLoader源码剖析

ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里主要介绍ClassLoader中几个比较重要的方法。

loadClass

该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作:

再讲一遍类的加载过程:

1、那么app会先查找是否加载过A,若有,直接返回;

2、若没有,去ext检查是否加载过A,若有,直接返回;

3、若没有,去boot检查是否加载过A,若有,直接返回;

4、若没有,那就boot加载,若在E:\Java\jdk1.8\jre\lib*.jar下找到了指定名称的类,则加载,结束;

5、若没找到,boot加载失败;

6、ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext*.jar下找到了指定名称的类,则加载,结束;

7、若没找到,ext加载失败;

8、app加载,若在类路径下找到了指定名称的类,则加载,结束;

9、若没有找到,抛出异常ClassNotFoundException

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
scss复制代码    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//是否需要在加载时进行解析
if (resolve) {
resolveClass(c);
}
return c;
}
}

用指定的二进制名称来加载类,这个方法的默认实现按照以下顺序查找类:
调用findLoadedClass(String)方法检查这个类是否被加载过

使用父加载器调用loadClass(String)方法,如果父加载器为Null,类加载器装载虚拟机内置的加载器调

用findClass(String)方法装载类, 如果,按照以上的步骤成功的找到对应的类,并且该方法接收的resolve参数的值为true,那么就调用resolveClass(Class)方法来处理类。

defineClass

defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象,这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象。defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

1
2
3
4
5
6
7
8
9
10
scss复制代码 protected Class<?> findClass(String name) throws ClassNotFoundException {
// 获取类的字节数组
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
//使用defineClass生成class对象
return defineClass(name, classData, 0, classData.length);
}
}

但是如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(还在链接阶段,需要解析链接),其解析操作需要等待初始化阶段进行。

findClass(String)

从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的ClassLoader类中findClass()方法源码如下:

1
2
3
4
arduino复制代码//直接抛出异常 
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

resolveClass

resolveClass方法可以使用类的Class对象创建完成也同时被解析。前面说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

总结

看了很多大佬写的类加载器文章,但是自己总结一遍写一下,看一遍源码收获是实实在在的。

抽奖说明

1.本活动由掘金官方支持 详情可见juejin.cn/post/701221…

2.通过评论和文章有关的内容即可参加,要和文章内容有关哦!

3.本月的文章都会参与抽奖活动,欢迎大家多多互动!

4.除掘金官方抽奖外本人也将送出周边礼物(马克杯一个和掘金徽章若干,马克杯将送给走心评论,徽章随机抽取,数量视评论人数增加)。

本文转载自: 掘金

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

炸!1024我的故事,一个写了两年博客的大厂码农!

发表于 2021-10-25

作者:小傅哥

博客:bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

一、嗯,肝了两年

300篇文章、4本PDF、2个小册、1本出版图书,为自己折腾到日子让我兴奋!

两年来,11前睡觉,早上6:20起床洗漱🧽、7:20跑步回来🏃,写作✏️或看书一小时,到了周末基本就可以全时间投入到自己到这个小世界里:编写案例、整理博客、发布文章、技术交流、同好扯皮。

哈哈哈,有伙伴问傅哥,你咋这么卷!可能我自己到没觉得,因为做自己喜欢的事你会发现自己特别容易投入,也不需要所谓的鼓励来让自己坚持。对于每一项的知识学习都是为了可以不被别人的一两句话忽悠住,我就想扒开看看到底咋回事,为了这个到底,好家伙就一直冲到了现在。

截止到21年10月24日,我已经在写博客的路上足足有两年了,趁着这个1024的好日子,趁着黑夜,嘿嘿,我把博客偷偷升级到 vuepress 版本了,对于这件事我早已预谋已久。因为最新版的博客 bugstack.cn 可以增强体验、利于阅读、开放文章、支持PR,共同维护、一起进步!

❤️初心,沉淀、分享、成长,让自己和他人都能有所收获!

二、对,关于作者

你好,我是小傅哥,《重学Java设计模式》图书作者,一线互联网 Java 工程师、架构师。

一个着迷于技术又喜欢不断折腾的技术活跃者,从13年毕业到进入互联网,开发过交易、营销类项目,实现过运营、活动类项目,设计过中间件,组织过系统重构,编写过技术专利。不仅从事业务系统的开发工作,也经常做一些字节码插桩类的设计和实现,对架构的设计和落地有丰富的经验。在热衷于Java语言的同时,也喜欢研究中继器、I/O板卡、C#和PHP!

除此之外小傅哥并不只满足于CRUD搬砖,也关心业务、运营、产品、数据、测试、运维等各项知识体系的完善学习,就研发架构设计来讲,更全面的学习会更有利于做出更长远的架构设计。同时完善个人知识体系也更有利于个人成长。

所以你会看到小傅哥在工作之外的深夜、周末、假期会折腾于写文章、编小册、出书籍,并十分热情于对粉丝的交流、提问、解惑。并不深沉且少许逗比的我,希望能给大家带来最接地气的帮助和成长。

我给自己在技术职业成长上,定位成一个能抗住农夫三拳的架构师,所以我在编写和输出的技术内容上,也是以数据结构、算法逻辑、设计模式、核心技术、系统架构、服务运维等方面的知识扩展技术广度和深度,并以实践验证的学习方式进行汇总内容编写文章。也希望这些成体系的技术系列内容能帮助你慢慢且踏实的成长起来。

三、嘿,历史记录

  • 2009年-2013年,在大学学习 Java 编程,并结交了很多小伙伴,大帝、小黎子、糖糖、苏二毛、蚂蚁等
  • 2013年,入职传统烟草行业,从一个初学Java程序猿开始写C#,并跟随飞哥四处出差部署项目;上海、滁州、长春、邢台,从此不在是Java程序猿还会C#、PHP、C++、IO板卡、PLC、中继器。
  • 2015年-至今,跳槽到互联网大厂。逐步参与和编写较大型项目以及中间件开发。
  • 2019年,逐步开始带领技术小组承担项目开发设计相关工作。
  • 2019年,创建 bugstack虫洞栈 | 沉淀、分享、成长,专注于原创专题案例编写,让自己和他人都能有所收获。目前已完成;Netty4.x专题案例、用Java实现JVM、手写RPC框架、基于JavaAgent链路监控等
  • 2019年,重新开始在 CSDN 写博客,并成为博客专家。
  • 2019年11月份,微信公众号bugstack虫洞栈突破1k读者。
  • 2020年02月份,与 GitChat 合作了第一个付费专栏《Netty+JavaFx实战:仿桌面版微信聊天》
  • 20年03月,总结职场类文章
  • 20年04月,编写ASM、Javassist、Byte-Buddy,字节码编程系列文章
  • 20年05月-07月,编写专栏《重学Java设计模式》,并推出PDF书籍,全网下载量14万+
  • 20年08月-12月,推出两个大专栏《面经手册 • 拿大厂Offer》、《码场故事》
  • 21年04月23日,图书节,我的第一本技术书《重学Java设计模式》出版了,G哥、敖丙、帅地、cxuan、Hollis、小林、小灰总、付东来,开涛大佬,都支持了我,哈哈哈
  • 21年06月20日,累计全网12万粉丝
  • 21年10月24日,博客从 jekyll 升级到 vuepress 并开源所有文章。感谢 @pdai 提供模版!
  • 我写过最好的一句话是:承遇朝霞、年少正恰、整装戎马、刻印风华。
  • 我是小傅哥,喜欢并热爱编程,执着于努力之后所带来的美好生活!

四、冲,新版博客

全新UI、支持搜索、清晰的分类和目录、沉浸式的阅读、看书一样的体验


1. 博客分类

而这几大块内容也是每一个较贵的 Java 程序员应该掌握的内容,可以包括:

  • Java&Spring:以讲解Java、Spring核心知识为基础,用数学逻辑思维分析关于Java、Spring、Mybatis、Dubbo等核心源码技术内容。其中如《Java 面经手册》是一本以面试题为入口讲解 Java 核心内容的技术书籍,书中内容极力的向你证实代码是对数学逻辑的具体实现。包括正在编写的《手撸 Spring》通过手写简化版 Spring 框架,了解 Spring 核心原理。在手写的过程中会简化 Spring 源码,摘取整体框架中的核心逻辑,简化代码实现过程,保留核心功能,例如:IOC、AOP、Bean生命周期、上下文、作用域、资源处理等内容实现。这些都程序员学习技术成长过程中非常重要的知识,如果能深入学习那么对以后的个人成长帮助非常大。
  • 算法逻辑和数据结构:这部分内容主要以Java源码为入手,讲解其中的数学知识,包括:扰动函数、负载因子、拉链寻址、开放寻址、斐波那契(Fibonacci)散列法还有黄金分割点的使用等等,这也正式《Java 面经手册》的核心内容所在。
  • 面向对象:《Java 设计模式》的知识是在Java基础铺平,数据结构、算法逻辑有了一定的了解后,在深入学习和使用的技术。同样是一个需求在学过设计模式后,也阅读了不少别人优秀的代码,那么在他实现需求的时候,会拆分出很多的接口和接口的继承、抽象类的职责隔离实现、具体业务模块的分层、功能服务组件的细化、具体实现过程中对设计模式的运用等等。这样的代码实现后会非常具有易扩展和可维护的特点,否则一篇的ifelse不是坑自己就是坑下一个人。
  • 中间件:可能很大一部分研发并不会接触到中间件,也不太可能有人告诉你可以使用中间件的方式解决一些实际遇到的问题。因为大部分时候你都会认为中间件只是公司专门部门的人写的,或者是技术大牛搞的,总之与你没关系。但其实代码知识对数学逻辑的具体实现,业务开发有业务开发的方式,《Spring 中间件和开发》也只是对Spring的关于容器中一些特定接口和类的使用,具体的还是普通的逻辑代码,比如暴露服务、采集日志、监控系统等。但如果你能早些学到这样技术的核心思想,那么对于升值、加薪、跳槽,都是非常有帮助的。
  • 通信专题:其实Netty是一项非常重要的技术,比如在RPC服务实现中的Dubbo、或者MQ、以及很多时候的通信里都是能用到的技术。就连小傅哥的第一次面试大厂也是靠着对Netty的学习,刷进来的!所以小傅哥编写了很多Netty从基础入门讲解到核心原理,告诉你如何处理半包、粘包,怎样定义消息协议,并开发了一个基于Netty的仿微信聊天项目,这些技术内容你都可以在我的博客学习到学习到。
  • 字节码编程:这项技术可能大多数研发,哪怕35岁的,可能也不一定接触到。但这样的技术你却基本都用过,比如你的IDEA是购买的吗,你怎么给让它能用的!你用过一些非入侵的全链路监控系统的,你通过字节码插桩搞过一些事情吗,那你用过Cglib吧,它的底层就是通过ASM字节码框架对字节码进行的一些列操作。
  • 关于:除了技术学习以外,还有很多伙伴会经常问我一些关于学习、成长以及在职场中怎么活下去。所以我结合我自己在大厂互联网中的学习和成长经历,给读者伙伴写了不少此类的内容。如简历编写、招聘要求、技术资料、代码规范、评审晋升、薪资待遇、副业收入等等。这些内容可能很多会帮助你度过一个安定的职场生涯!

2. 站点地图

  • 在文章阅读的都站点地图中你可以快速找到常用信息,包括:技术社区、PDF 下载、专栏资料、项目开发、知识星球等,如果你还有其他特别需要的,总是使用的也可以联系小傅哥进行添加。

3. 文章开源

  • 所有的文章都支持定位到 github 的 CodeGuide 对应的文章中,支持提交修改,也支持提交PR。这样可以更大限度的满足同好对本仓库的共建,让这份支持变更更加有力量,也可以让每个人都能参与到这样一个已经6k Star🌟的项目上。
  • 项目地址:github.com/fuzhengwei/… - 非常有价值!

4. 阅读解锁

  • 增长文章解锁🔓时效,只要你的浏览器对 cookie 没有限制,或者你没有定期删除,那么文章在当前浏览器下会一直处于有效状态。
  • 如果你不能正常解锁,可以在文章的顶部点击阅读原文,这些文章是博客的原文地址(陆续补充中),也可以在 CodeGuide 中阅读(打开速度很慢),再有就是找傅哥帮忙。
  • 所有的加锁都只是为了让这份创作可以继续下去,除了热情在这件事上,还有很多经历、成本、支出需要回馈一些,否则真的很难坚持下去。经费这块都难以为继! 感谢理解,真的不是为了阻挡你阅读!

5. 其他功能

在这个最新的博客模版中还提供了其他增强阅读的功能,包括:手机扫码阅读、关闭侧边栏放出最大可视区域,图片点击放大、连贯的上下篇阅读等小功能,可以更好的满足你在阅读学习时体验诉求。

同时为了老用户已经保留了旧版博客的地址,以及 CodeGuide Wiki 的文章链接使用的都是旧版博客地址,所以以前版本的博客的内容并没有删除,依旧可以使用。只有你跳回首页时才会进去到新的版本,后续旧版博客链接调用量逐渐放缓减少后,全部切换为新版博客。

五、来,送个福利

中间件小册 - 5折码

  • 《SpringBoot 中间件设计和开发》:全小册19个章节,包括16个中间件的设计和开发,包括测试案例共30个代码库提供给读者学习使用。小册实现的中间件场景涵盖:技术框架、数据服务、数据组件、分布式技术、服务治理、字节码、IDEA插件七个方面,贯穿整个互联网系统架构中常用的核心内容。非常值得了解、学习、实践到掌握。

小册 5 折优惠

六、吼,感谢读者

一路走来感谢大家对支持、认可、帮助,也感谢那么多的伙伴分享小傅哥的博客、公众号、PDF、小册、书籍到自己的群和朋友圈中,我总能看见你们在那里支持小傅哥,真的非常感谢!!!

我会在这条路上一直走技术路线,坚持输出有价值的技术内容,与同好一起进步成长。就像我所坚持的那样,沉淀、分享、成长,让自己和他人都能有所收获!

本文转载自: 掘金

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

SpringBoot集成Zookeeper结合AOP实现分布

发表于 2021-10-25

​引言

在程序开发过程中不得不考虑的就是并发问题。在java中对于同一个jvm而言,jdk已经提供了lock和同步等。但是在分布式情况下,往往存在多个进程对一些资源产生竞争关系,而这些进程往往在不同的机器上,这个时候jdk中提供的已经不能满足。分布式锁顾明思议就是可以满足分布式情况下的并发锁。下面我们讲解怎么利用zk实现分布式锁。

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

springBoot集成zk实现分布式锁目录结构

image.png

zookeeper配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@Configuration
@EnableConfigurationProperties(ZkProps.class)
public class ZkConfig {
private final ZkProps zkProps;

@Autowired
public ZkConfig(ZkProps zkProps) {
this.zkProps = zkProps;
}

@Bean
public CuratorFramework curatorFramework() {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(zkProps.getTimeout(), zkProps.getRetry());
CuratorFramework client = CuratorFrameworkFactory.newClient(zkProps.getUrl(), retryPolicy);
client.start();
return client;
}
}
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
less复制代码/**
* <p>
* Zookeeper 配置项
* </p>
*
* @description: Zookeeper 配置项
*/
@Data
@ConfigurationProperties(prefix = "zk")
public class ZkProps {
/**
* 连接地址
*/
private String url;

/**
* 超时时间(毫秒),默认1000
*/
private int timeout = 1000;

/**
* 重试次数,默认3
*/
private int retry = 3;
}

自定义分布式锁注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
less复制代码/**
* <p>
* 分布式锁动态key注解,配置之后key的值会动态获取参数内容
* </p>
*
* @description: 分布式锁动态key注解,配置之后key的值会动态获取参数内容
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockKeyParam {
/**
* 如果动态key在user对象中,那么就需要设置fields的值为user对象中的属性名可以为多个,基本类型则不需要设置该值
* <p>例1:public void count(@LockKeyParam({"id"}) User user)
* <p>例2:public void count(@LockKeyParam({"id","userName"}) User user)
* <p>例3:public void count(@LockKeyParam String userId)
*/
String[] fields() default {};
}
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
less复制代码/**
* <p>
* 基于Zookeeper的分布式锁注解
* 在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁
* </p>
*
* @description: 基于Zookeeper的分布式锁注解,在需要加锁的方法上打上该注解后,AOP会帮助你统一管理这个方法的锁
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ZooLock {
/**
* 分布式锁的键
*/
String key();

/**
* 锁释放时间,默认五秒
*/
long timeout() default 5 * 1000;

/**
* 时间格式,默认:毫秒
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

日志切入

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
101
java复制代码@Aspect
@Component
@Slf4j
public class ZooLockAspect {
private final CuratorFramework zkClient;

private static final String KEY_PREFIX = "DISTRIBUTED_LOCK_";

private static final String KEY_SEPARATOR = "/";

@Autowired
public ZooLockAspect(CuratorFramework zkClient) {
this.zkClient = zkClient;
}

/**
* 切入点
*/
@Pointcut("@annotation(com.xkcoding.zookeeper.annotation.ZooLock)")
public void doLock() {

}

/**
* 环绕操作
*
* @param point 切入点
* @return 原方法返回值
* @throws Throwable 异常信息
*/
@Around("doLock()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Object[] args = point.getArgs();
ZooLock zooLock = method.getAnnotation(ZooLock.class);
if (StrUtil.isBlank(zooLock.key())) {
throw new RuntimeException("分布式锁键不能为空");
}
String lockKey = buildLockKey(zooLock, method, args);
InterProcessMutex lock = new InterProcessMutex(zkClient, lockKey);
try {
// 假设上锁成功,以后拿到的都是 false
if (lock.acquire(zooLock.timeout(), zooLock.timeUnit())) {
return point.proceed();
} else {
throw new RuntimeException("请勿重复提交");
}
} finally {
lock.release();
}
}

/**
* 构造分布式锁的键
*
* @param lock 注解
* @param method 注解标记的方法
* @param args 方法上的参数
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private String buildLockKey(ZooLock lock, Method method, Object[] args) throws NoSuchFieldException, IllegalAccessException {
StringBuilder key = new StringBuilder(KEY_SEPARATOR + KEY_PREFIX + lock.key());

// 迭代全部参数的注解,根据使用LockKeyParam的注解的参数所在的下标,来获取args中对应下标的参数值拼接到前半部分key上
Annotation[][] parameterAnnotations = method.getParameterAnnotations();

for (int i = 0; i < parameterAnnotations.length; i++) {
// 循环该参数全部注解
for (Annotation annotation : parameterAnnotations[i]) {
// 注解不是 @LockKeyParam
if (!annotation.annotationType().isInstance(LockKeyParam.class)) {
continue;
}

// 获取所有fields
String[] fields = ((LockKeyParam) annotation).fields();
if (ArrayUtil.isEmpty(fields)) {
// 普通数据类型直接拼接
if (ObjectUtil.isNull(args[i])) {
throw new RuntimeException("动态参数不能为null");
}
key.append(KEY_SEPARATOR).append(args[i]);
} else {
// @LockKeyParam的fields值不为null,所以当前参数应该是对象类型
for (String field : fields) {
Class<?> clazz = args[i].getClass();
Field declaredField = clazz.getDeclaredField(field);
declaredField.setAccessible(true);
Object value = declaredField.get(clazz);
key.append(KEY_SEPARATOR).append(value);
}
}
}
}
return key.toString();
}

}

测试类

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
java复制代码@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class SpringBootDemoZookeeperApplicationTests {

public Integer getCount() {
return count;
}

private Integer count = 10000;
private ExecutorService executorService = Executors.newFixedThreadPool(1000);

@Autowired
private CuratorFramework zkClient;

/**
* 不使用分布式锁,程序结束查看count的值是否为0
*/
@Test
public void test() throws InterruptedException {
IntStream.range(0, 10000).forEach(i -> executorService.execute(this::doBuy));
TimeUnit.MINUTES.sleep(1);
log.error("count值为{}", count);
}

/**
* 测试AOP分布式锁
*/
@Test
public void testAopLock() throws InterruptedException {
// 测试类中使用AOP需要手动代理
SpringBootDemoZookeeperApplicationTests target = new SpringBootDemoZookeeperApplicationTests();
AspectJProxyFactory factory = new AspectJProxyFactory(target);
ZooLockAspect aspect = new ZooLockAspect(zkClient);
factory.addAspect(aspect);
SpringBootDemoZookeeperApplicationTests proxy = factory.getProxy();
IntStream.range(0, 10000).forEach(i -> executorService.execute(() -> proxy.aopBuy(i)));
TimeUnit.MINUTES.sleep(1);
log.error("count值为{}", proxy.getCount());
}

/**
* 测试手动加锁
*/
@Test
public void testManualLock() throws InterruptedException {
IntStream.range(0, 10000).forEach(i -> executorService.execute(this::manualBuy));
TimeUnit.MINUTES.sleep(1);
log.error("count值为{}", count);
}

@ZooLock(key = "buy", timeout = 1, timeUnit = TimeUnit.MINUTES)
public void aopBuy(int userId) {
log.info("{} 正在出库。。。", userId);
doBuy();
log.info("{} 扣库存成功。。。", userId);
}

public void manualBuy() {
String lockPath = "/buy";
log.info("try to buy sth.");
try {
InterProcessMutex lock = new InterProcessMutex(zkClient, lockPath);
try {
if (lock.acquire(1, TimeUnit.MINUTES)) {
doBuy();
log.info("buy successfully!");
}
} finally {
lock.release();
}
} catch (Exception e) {
log.error("zk error");
}
}

public void doBuy() {
count--;
log.info("count值为{}", count);
}

}

本文转载自: 掘金

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

OSM学习之路(六):基于Ubuntu搭建逆地理服务器(OS

发表于 2021-10-25

1、数据准备

从download.geofabrik.de/asia.html中下…

从planet.openstreetmap.org/pbf/中下载世界地图…

从 www.nominatim.org/release/Nom…

2、必要软件安装

用户状态:jtrj

桌面—右键—打开终端,逐个录入如下命令 ;

sudo apt-get install build-essential wget

sudo apt-get install libxml2-dev wget

sudo apt-get install libpq-dev wget

sudo apt-get update

sudo apt-get install libbz2-dev wget

sudo apt-get install libtool wget

sudo apt-get install automake wget

sudo apt-get install libproj-dev wget

sudo apt-get install libboost-dev wget

sudo apt-get install libboost-system-dev wget

sudo apt-get install libboost-filesystem-dev wget

sudo apt-get install libboost-thread-dev wget

sudo apt-get install libexpat-dev wget

sudo apt-get install gcc wget

sudo apt-get install proj-bin wget

sudo apt-get install libgeos-c1v5 wget

sudo apt-get install libgeos++-dev wget


ubuntu上默认只能安装php7,而nominatim需要安装php5,所以安装破坏

php5时需要执行多步命令。逐个执行下列指令;

sudo add-apt-repository ppa:ondrej/php

image.png

sudo apt update

sudo apt install php5.6

sudo apt install libapache2-mod-php5.6

sudo apt install php5.6-curl

sudo apt install php5.6-gd

sudo apt install php5.6-mbstring

sudo apt install php5.6-mcrypt

sudo apt install php5.6-mysql

sudo apt install php5.6-xml

sudo apt install php5.6-xmlrpc

sudo a2dismod php7.0 //系统上如有php7.0,则此命令卸载7.0版本

sudo a2enmod php5.6

sudo systemctl restart apache2


sudo apt-get install php-pear wget

sudo apt-get install php5.6-pgsql wget

sudo apt-get install php5.6-json wget

sudo apt-get install php-db wget

sudo apt-get install osmosiswget

sudo apt-get install postgresql-9.5 wget

sudo apt-get install postgis

sudo apt-get install postgresql-contrib-9.5 wget

sudo apt-get install postgresql-server-dev-9.5 wget

sudo apt-cache search postgres //获取插件列表查看postgis版本

sudo apt-get install postgresql-9.5-postgis-2.2

//postgresql- 9.5插件为postgis-2.2,postgresql-9.6插件为postgis-2.3

安装pbf支持软件:

sudo apt-get install libprotobuf-c0-dev

sudo apt-get install protobuf-c-compiler

安装postgre可视化窗口pgadmin3:

1)wget –quiet -O - www.postgresql.org/media/keys/… | sudo apt-key add - //不要落下最后的横线,前面那个是大写字母O,而非数字0

2) sudo apt-get update

3) sudo apt-get install postgresql-client-9.5

4) sudo apt-get install pgadmin3

5) pgadmin3 //打开pgadmin3,以确认是否正确安装。直接关闭窗口则终端也退出。

3、配置postgresql

用户状态:jtrj

  1. 1
    复制代码设置postgres用户,用postgres用户登录并修改密码。

sudo -u postgres psql postgres

\password postgres //密码设置为postgres即可

\q

image.png

pgadmin3 //以下图片中的密码即为上一张图片中设置的密码

image.png

  1. 配置postgresql

sudo gedit /etc/postgresql/9.5/main/postgresql.conf

//注意gedit后有空格。必须使用命令打开.conf文件,否则修改之后没有权限,不能保存。

去掉 “#” 号,修改参数

shared_buffers = 2GB 113 行

work_mem = 50MB 122 行

maintenance_work_mem = 10GB 123 行

fsync = off 173 行

synchronous_commit= off 174 行

full_page_writes = off 183 行

checkpoint_timeout = 10min 196 行

checkpoint_completion_target = 0.9 199 行

effective_cache_size = 24GB 289 行

4、编译Nominatim

用户状态:jtrj

  1. 下载Nominatim

默认下载到 /home/jtrj/目录下,点击鼠标右键—提取,进行解压即可,然后逐个执行以下指令;

2) 编译Nominatim

cd /home/jtrj/下载/Nominatim-2.5.1 //第一条指令,注意cd后有空格

./configure //第二条指令,注意最前面是“.”,执行这个指令时如果出现lua错误或警告,需执行下面两条指令,再执行make指令;

sudo apt-get install libreadline-dev

sudo apt-get install lua5.2 lua5.2-doc liblua5.2-dev

make //第三条指令,最后三句话用于编译nominatim,执行make指令需要点时间,请耐心等待。

  1. 设置nominatim的网络位置

在Nominatim目录中的settings目录里新建local.php文件,告知nominatim它在网络服务器上的位置,新建文档内容如下:

`<?php

// Paths

@define(‘CONST_Postgresql_Version’, ‘9.5’);

@define(‘CONST_Postgis_Version’, ‘2.2’);

// Website settings

@define(‘CONST_Website_BaseURL’, ‘[http://localhost/nominatim/');`](http://localhost/nominatim/');%60)

5、创建导入账户,用于导入数据

用户状态:先是普通用户jtrj,创建完test用户后,切换到test用户,逐条语句执行。

sudo -u postgres createuser -s test (postgres的)

sudo adduser test //(test密码随意,123456)

sudo passwd root //第一次进入操作系统,根用户root默认没有密码,因此需要先设置root用户密码,根据提示录入新密码即可。

su root //切换到root用户

chmod 777 /etc/sudoers

打开文件,进入 /etc目录,打开sudoers文件;

在”root ALL=(ALL) ALL “下添加:test ALL=(ALL) ALL

然后保存退出,再修改回sudoers的状态;

chmod 440 /etc/sudoers

exit

su test //切换到test用户,密码:123456

createuser -SDR www-data (创建postgres用户) //创建后记得要打开看一下是否成功,需要切换到jtrj用户查看

image.png

6、导入数据

用户状态:test

(1)如果需要导入全球数据,可进入到这个网站下载全球地图数据:

从planet.openstreetmap.org/pbf/中下载世界地图…

执行导入操作:

/home/jtrj/下载/Nominatim-2.5.1/utils/setup.php –osm-file /home/jtrj/下载/plant-190422.osm.pbf –all //向数据库中导入数据。此语句不能复制粘贴,只能手动输入!

//osm数据也存放到/home/jtrj/下载 目录下,方便操作

(2)如果需要合并两个地区的数据,可进入到如下网站下载各地区数据:

从download.geofabrik.de/asia.html中下…

执行合并操作:

osmosis –read-pbf file=”/home/some/下载/areaA.osm.pbf” –read-pbf file=”/home/some/下载/areaB.osm.pbf” –merge –write-pbf file=”/home/some/下载/areaA-areaB.osm.pbf” //合并多个国家地区的数据, areaA-areaB.osm.pbf中的areaA-areaB是自己命名。

合并完毕后再执行导入操作,语句同全球导入数据一样,更换osm文件名即可。

注意:
(1)这里,如果刚执行命令时报 php找不到DB的错误,执行 sudo pear install DB
(2)如果是导入过程中出现错误,要删除数据库再重新导入, 删除命令为:

sudo -u postgres dropdb nominatim

(3)在导入过程中,可能会出现如下的报错情况:

index_placex: UPDATE failed: ERROR: buffer 238141 is not owned by resource owner PortalCONTEXT: SQL statement “INSERT INTO search_name_95 values (in_place_id, in_rank_search, in_rank_address, in_name_vector, in_geometry)”PL/pgSQL function insertsearchname(integer,bigint,character varying,integer[],integer[],integer,integer,double precision,geometry,geometry) line 436 at SQL statement PL/pgSQL function placex_update() line 359 at assignment

ERROR: Error executing external command: /home/jtrj/下载/Nominatim-2.5.1/nominatim/nominatim -i -d nominatim -P 5432 -t 15 -r 26

Error executing external command: /home/jtrj/下载/Nominatim-2.5.1/nominatim/nominatim -i -d nominatim -P 5432 -t 15 -r 26

继续执行指令:

/home/jtrj/下载/Nominatim-2.5.1/nominatim/nominatim -i -d nominatim -P 5432 -t 15 -r 26 执行该指令的时候如果出现提示ubuntu16.04系统内部错误之类的,建议重启服务器再执行,该条指令需要等待很长时间,可能长达24小时,请耐心等待。

在执行过程中会反复出现类似的报错或其他报错,继续执行该条指令直到导入完毕即可。

(4)添加额外字段(以下语句可待完全配置成功后再执行)

/home/jtrj/下载/Nominatim-2.5.1/utils/specialphrases.php–countries > /home/jtrj/下载/Nominatim-2.5.1/data/specialphrases_countries.sql

su test //切换到test用户

psql -d nominatim -f /home/jtrj/下载/Nominatim2.5.1/data/specialphrases_countries.sql

//此三句是在搜索索引中添加国家/地区代码和国家/地区

Exit

/home/jtrj/下载/Nominatim-2.5.1/utils/specialphrases.php –wiki-import > /home/jtrj/下载/Nominatim-2.5.1/data/specialphrases.sql

su test //切换到test用户

psql -d nominatim -f /home/jtrj/下载/Nominatim-2.5.1/data/specialphrases.sql

//搜索具体或特殊设施时需要导入特殊的短语

exit

7、建立网站

用户状态:test

(1)创建网站的目录,并确保它是可写的安装用户和可读:

sudo mkdir -m 755 /var/www/html/nominatim

sudo chmod 777 /var/www/html/nominatim //注意数字之后都有一个空格

(2)使用必要的符号链接填充网站目录:

/home/jtrj/下载/Nominatim-2.5.1/utils/setup.php –create-website

/var/www/html/nominatim //这两行文字是一条指令,/var前面有一个空格

(3)配置apache环境

sudo gedit /etc/apache2/sites-enabled/000-default.conf //打开.conf文件

在最后添加如下内容

`<Directory “/var/www/html/nominatim/“>

1
2
3
markdown复制代码        Options FollowSymLinks MultiViews

AddType text/html .php

`

(4)修改后重启Apache

service apache2 restart // apache配置在更改后需要重启

(5)增加test对数据库nominatim的权限

su test

psql template1

GRANT ALL PRIVILEGES ON DATABASE nominatim to test

//此处test是创建的库,注意按自己的名称

\q

Exit

(6)运行nominatim

浏览器登陆http://localhost/nominatim/ 注意,不论是否联网,可以查到的名称地点

应该全都属于你所导入的地区。当联网时会出现具体的地图,当不联网时只能看到轮廓。

打开数据库查看www-data用户有没有数据库读写权限,若没有则执行以下命令:

sudo gedit /etc/apache2/envvars

按如下内容修改打开的文件:

exportAPACHE_RUN_USER=test

exportAPACHE_RUN_GROUP=test

(7)局域网设置

按如下内容修改local.php:

`<?php

// Paths

@define(‘CONST_Postgresql_Version’, ‘9.5’);

@define(‘CONST_Postgis_Version’, ‘2.2’);

// Website settings

@define(‘CONST_Website_BaseURL’, ‘http://000.000.000.000/nominatim/‘);

`
以上000.000.000.000为电脑IP

修改后重启Apache

service apache2 restart

8、修改IP地址为静态IP

(1)点击桌面右上角的网络图标,选择 Edit Connections;

image.png

(2)选中ens33,点击Edit按钮;

image.png

(3)进入编辑页面,点击Ipv4 Settings,如图:

image.png

(4)Method选择:Manual;录入该电脑的静态IP、网关和DNS;

可在桌面上打开终端,输入如下命令查找IP等信息;

image.png

查看DNS

image.png

9、设置局域网
打开Nominatim-2.5.1/settings/local.php 文件,将最后一行的:

http://localhost/nominatim

改为:

http://192.168.1.43/nominatim # 192.168.1.43是我服务器的静态IP(根据自己的情况修改)

后面我整理一下nominatim使用的API,到时候我们开发应用可以直接使用OSM来做开发。

其中遇到过一些问题,可能在导入中会出现错误,如果只是使用反地理编码可以继续执行文中提到的那条语句,最后也能完全导入。

后面我更换了自己用IP SAN改造的那台性能强悍的服务器后,大概只用了一个星期的时间,没有任何问题的执行完成了。

image.png

本文转载自: 掘金

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

gin框架实践【Go-Gin_Api】20全新升级

发表于 2021-10-25

引言

  • 终于在经过一段时间的打磨,2.0出现了,方便大家对gin框架的学习
  • github传送门
  • 喜欢的铁子们给点个star

1.支持功能

  1. 支持Swagger接口文档生成
  2. 支持jwt鉴权
  3. 支持zap 日志
  4. 支持viper 配置文件解析
  5. 支持go1.6.0 go:embed特性,打包包含静态文件
  6. 支持gorm 数据库组件、支持读写分离,数据库主从
  7. 支持web界面 使用 Light Year Admin 模板,vue学习有点成本
  8. 支持多角色的RBAC权限控制,使用casbin
  9. 后续支持工具生成项目
  10. 支持热编译fresh
  1. 在线文档

1
2
3
复制代码1、使用gitbook生成
2、使用github的pages功能设置
3、文档部分待完善

文档地址

  1. 更新后的目录架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arduino复制代码 ├─app  	     (项目核心目录)
| ├─controller (控制器)
| ├─middleware (中间件)
| ├─models (数据结构层)
| ├─request (数据请求层,定义特殊请求结构体以及数据校验)
| ├─request (数据展示层定义结构体)
| ├─services (服务层)
├─config (配置包)
├─core (內核)
├─docs (swagger文档目录)
├─global (全局变量)
├─initialize (初始化)
├─routes (路由)
├─static (静态文件包括config目录)
├─templates (模板)
├─tests (测试)
└─tool (工具)
  1. 项目图片

login

login

login

  1. 后续计划

  • 支持命令工具生成model、controller、request等等
  • 后台支持操作日志
  • 后台支持计划任务
  • 支持配置管理(尽量配置化)
  • 支持cache
  • 等等
  1. 系列文章

  • 连载一 golang环境搭建
  • 连载二 安装Gin
  • 连载三 定义目录结构
  • 连载四 搭建案例API1
  • 连载五 搭建案例API2
  • 连载六 接入swagger接口文档
  • 连载七 日志组件
  • 连载八 优雅重启和停止
  • 连载番外 Makefile构建
  • 连载番外 Cron定时任务
  • 连载番外 打造命令行工具
  • 连载番外 3天打造专属Cache(First day)
  • 连载番外 3天打造专属Cache(Second day)
  • 连载番外 3天打造专属Cache(Third day)

本文转载自: 掘金

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

Springboot多种方法处理静态资源:设置并访问静态资源

发表于 2021-10-25

作者:Mintimate

博客:www.mintimate.cn
Mintimate’s Blog,只为与你分享

静态文件

静态资源,一般是网页端的:HTML文件、JavaScript文件和图片。尤其是设置图片的静态资源,尤其重要:
静态资源图片)静态资源图片这样的静态资源访问不会被Springboot所拦截处理(方便用于CDN加速):
Springboot日志并没有显示

虽然真实项目里,图片可以直接存储在对象存储的存储桶内或者直接用Nginx进行反代,但是一些小的静态资源,直接Springboot规划静态资源,也是个不错的选择。

Springboot内设置静态资源,或者说静态资源文件夹,主要有两种方法(均为SpringMVC实现):

  • 在application.yml/application.properties内配置。
  • 设置Configuration配置类。

更多内容,可以参考Spring官方文档:www.baeldung.com/spring-mvc-…

以上两种方法,均可实现用户访问网址,不走Controller层的拦截,直接进行静态文件访问:

简单解释一下

application设置方法

配置详解

设置application方法很简单,主要涉及两个配置项:

  • spring.mvc.static-path-pattern:根据官网的描述和实际效果,可以理解为静态文件URL匹配头,也就是静态文件的URL地址开头。Springboot默认为:/**。
  • spring.web.resources.static-locations:根据官网的描述和实际效果,可以理解为实际静态文件地址,也就是静态文件URL后,匹配的实际静态文件。Springboot默认为:classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/

如何运作的?,这里我画个简单的图:
简单演示
需要注意:

  • spring.web.resources.static-locations是后续配置,旧版Springboot的配置项为:spring-resources-static-locations;在2.2.5版本之后,旧版本配置已经失效。
  • spring.web.resources.static-locations有多个配置项,在Springboot编译后,会合并为一个文件。多个配置文件,使用,进行分割。
  • spring.web.resources.static-location仅仅允许一个配置,无法使用,进行分割,如果需要多个静态资源文件,可以使用下文的配置类方法。
  • spring.web.resources.static-locations可以使用classpath、file进行匹配。如果使用file,这个时候的相对路径为项目地址(打包为.jar后,相对路径就是.jar运行地址)。

编写配置

现在,官方描述,我们已经知道了配置项的含义。现在我们就来配置。

我使用的是YML格式的application配置,如果你是使用XML格式的application.properties,记得进行更改。

最终效果很简单,我想要的效果:
浏览器输入:http://localhost:8088/SystemData/UserData/Avatar/Mintimate.jpeg
可以直接访问项目文件下的:/SystemData/UserData/Avatar/Mintimate.jpeg
就是这个文件了嗷
为了实现这样的效果,我们编写配置文件:

1
2
3
4
5
6
7
8
yml复制代码spring:
mvc:
# URL响应地址(Springboot默认为/**)
static-path-pattern: /SystemData/**
web:
resources:
# 静态文件地址,保留官方内容后,进行追加
static-locations: classpath:/static,classpath:/public,classpath:/resources,classpath:/META-INF/resources,file:SystemData

其中,file:SystemData就是映射本地文件了。

这样的配置,类似于Nginx的正则匹配:

1
2
3
nginx复制代码location ^~/SystemData{
alias /www/myWeb/SystemData;
}

这样,我们运行项目,就可以直接访问静态资源了:
直接访问静态资源成功
当然,这样有一些缺点……

优缺点

这样的配置,可以说最简单且粗暴,但是灵活性差一点点:

  • URL响应地址只能为一项,也就是spring.mvc.static-path-pattern配置只能写一项。

这意味着,按我上文设置了/SystemData/**为URL匹配,就不能设置第二个/resources/**这样的配置为第二静态目录。

如果需要设置多个地址为静态资源目录,可以参考下文的设置配置类方法方法。

设置配置类方法

配置详解

写一个配置类,实现静态资源的文件夹方法很多。比如:

  • 继承于WebMvcConfigurationSupport父类,并实现addResourceHandlers方法。
  • 引用WebMvcConfigurer接口,并实现addInterceptors方法

一些文章可能会让你继承于WebMvcConfigurerAdapter方法,但是实际上WebMvcConfigurerAdapter方法在Spring5.0和Springboot2.0之后,已经弃用。

这里,我处于习惯,就使用WebMvcConfigurationSupport进行实现addResourceHandlers:

1
2
3
java复制代码@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
}

这里的registry使用链式编程,方法为:

  • addResourceHandler:添加URL响应地址目录。
  • addResourceLocations:添加实际资源目录。

和application.yml里设置一样,支持classpath和file等关键词。接下来,我们就看看实际编写配置。

编写配置

现在我们就来配置。
最终效果很简单,我想要的效果(两组同时):

  • 浏览器输入:http://localhost:8088/SystemData/UserData/Avatar/Mintimate.jpeg
    可以直接访问项目文件下的:/SystemData/UserData/Avatar/Mintimate.jpeg,
  • 浏览器输入:http://localhost:8088/SystemDataTest/UserData/Avatar/Mintimate.jpeg
    可以直接访问项目文件下的:/Test/UserData/Avatar/Demo.jpeg,

本地资源目录文件夹
添加一个配置类,并继承WebMvcConfigurationSupport,实现addResourceHandlers方法,并打上@Configuration注解,使其成为配置类:
配置类
之后,重写内容:
重写内容
主要是:

1
2
3
4
5
java复制代码// 静态资源映射
registry.addResourceHandler("/SystemData/**")
.addResourceLocations("file:"+IMG_PATH);
registry.addResourceHandler("/SystemDataTest/**")
.addResourceLocations("file:"+IMG_PATH_TWO);

之后,浏览器就可以访问了:静态资源一)静态资源二
这样的配置,其实还是和Nginx配置类是……:
这样的配置,类似于Nginx的正则匹配:

1
2
3
nginx复制代码location ^~/SystemData{
alias /www/myWeb/SystemData;
}

当然,这样的优缺点……

优缺点

相比前文的配置,这样优缺点很明显:

  • 相比前文,这样的配置更麻烦。
  • 相比前文,这样的可塑性更高:可以添加更多的映射、不会对默认配置造成覆盖等。

总结

综上所述,就是Springboot的静态资源目录添加方法啦。是不是和Nginx很像?

虽然现在Nginx和对象存储都很方便,但是直接用Springboot进行静态资源的划分,也不为一种方法。

真不错

本文转载自: 掘金

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

OSM学习之路(四):将OSM 数据导入到PostgreSQ

发表于 2021-10-25

1.前提:

前几节简单介绍了一下osm与PostgreSQL,下面我们进入正题,如何将osm数据导入到PostgreSQL里面。
我选择的是利用osm2pgsql进行数据导入,osm2pgsql下载地址:pan.baidu.com/s/10KsOhOhz…
提取码:okyp。
导入OMS数据到PostgreSQL中

2.新建一个数据库

我们新建一个名称为osm的数据库,可以使用pgAdmin进行创建,和操作其他数据库类似,这里就不做赘述;也可以通过命令行创建,我在这里采取的是通过命令行创建数据库。

打开cmd命令窗口,进入数据库安装的根目录下,然后输入
createdb -U postgres -E UTF8 osm
image.png

这个过程可能会报错,无法连接到数据库,需要对数据配置文件进行简单的修改
image.png

找到数据库安装目录data目录下的pg_hba.conf,然后将红框对应得md5改成trust,然后再执行以下数据库创建语句则可创建成功,我的data目录是:D:\Program Files\PostgreSQL\10\data

image.png

修改配置文件后再次执行上面的命令即可完成数据库osm的创建

  1. 加载postgis.sql(postgis相关函数等)

同创建数据库一样dos命令进入PostgreSQL的安装目录的bin目录下,输入命令
psql -U postgres -d osm -f "D:\Program Files\PostgreSQL\10\share\contrib\postgis-2.4\postgis.sql"
(这里是我的目录,你们根据自己的安装路径进行调整)

image.png

image.png

执行完成后截图

image.png

  1. 加载spatial_ref_sys.sql(EPSG坐标系)

同2操作一样也是找到postgis-2.4目录下面的spatial_ref_sys.sql文件,执行
psql -U postgres -d osm -f "D:\Program Files\PostgreSQL\10\share\contrib\postgis-2.4\spatial_ref_sys.sql"

执行结束后截图

image.png

  1. 下面我们开始使用osm2pgsql将osm数据导入到PostgreSQL里面

(1)我们通过Dos命令进入我们osm2pgsql目录下,我这里是osm2pgsql放在了D盘的根目录下
image.png
(2)将我们下载的osm数据放在osm2pgsql目录下
image.png
(3)我们执行导入命令将osm数据导入到PostgreSQL里面
osm2pgsql -d osm -U postgres -P 5432 -C 25000 -S "D:\osm2pgsql\ default.style" asia-latest.osm

  1. -d:你的数据库名称
  2. -U:数据库的用户名
  3. -P:数据库的端口
  4. -C:数据缓存(根据你导入的数据的大小合理的设置缓存,如果设置的缓存太小,会导致由于缓存不足造成的导入失败)
  5. -S:选择导入时候用到的style文件
  6. asia-latest.osm:这个是我要导入的osm数据文件

开始执行导入命令后

image.png

我这次导入的是亚洲区域,解压后的osm数据有180G左右,导入过程比较耗时,大概用了1天半的时间完成了数据的导入

image.png

这时候表示 我的亚洲区域的数据导入成功

(4)开始导入其他区域的osm数据
osm2pgsql -a -d osm -U postgres -P 5432 -C 25000 -S "D:\osm2pgsql\ default.style" africa-latest.osm

这条命令我多加了一个-a,如果不加-a则会将我们之前导入的数据全部清空掉,只保留本次的导入,这个一定要注意,一不小心我们之前的工作就可能白费了。

image.png

后面又是一场漫长的等待…

我将8个分区的osm数据全部导入,一共花费了3周左右的时间,遇到了各种各样的问题,一般异常后继续执行就行

本文转载自: 掘金

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

1…470471472…956

开发者博客

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