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

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


  • 首页

  • 归档

  • 搜索

从Java8到Java17(二)

发表于 2021-11-08

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战。

上篇文章简单回顾了一下Java的版本历史,我们知道现在市场占有率最大的依然是Java8,如果想一步跨越到17可能存在一点困难,如刘姥姥进了大观园,新的东西太多导致目不暇接。可能也算不上太多,但也需要一步步去学习和掌握,首先从Java9开始,看看都有哪些变化。

Java9相对于8其实也是一个比较大的升级,尤其是引入了Java的模块化概念,但今天限于篇幅原因暂时还说不到这一块,先来看一下jdk的一些其他升级。

  1. 接口私有方法
    从最开始写Java的时候就知道Java的interface只用来写方法的声明,所有的方法都是默认public abstract,具体的实现要实现类自己去实现。后来Java8引入了default方法还是static方法,从而接口中也可以写方法的实现了。现在Java9可以在接口中写私有方法了,私有方法不需要复写,别的实现类也无法调用。可能Java官方希望淡化接口这种类型,减少一些不需要额外实现的工具方法的重复实现。
  2. Flow API
    这个东西的前身叫rxjava,或者叫reactive java。事件驱动已经不能简单的用来概括响应式编程了,为什么要增加这样的API呢,因为那段时间各个大厂纷纷发力异步编程,弄出很多种所谓的响应式框架,纷纷宣称自己性能好。后来被某个开源联盟联合起来一起出了一个响应式的规范:
  • Subscriber:订阅者
  • Publisher:生产者
  • Subscription:订阅关系
  • Processor:订阅者和生产者之间的N个处理步骤

响应式编程并不能提升多少性能,而是使程序更加稳定和获得更好的扩展性。

  1. 集合类的工厂方法
    Java经常被吐槽啰嗦,也确实啰嗦,创建一个小的集合要几步:
1
2
3
4
java复制代码Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.add("c");

然后再去使用这个set,四行代码,尤其是这个逐个add,不像一个现代语言。所以现在可以用简单的写法:

1
java复制代码Set<String> set = Set.of("a", "b", "c");

包括List,Set,Map都可以使用of来直接创建集合。

  1. Stream API的增强
    增加takeWhile, dropWhile, ofNullable, iterate的API,越来越像一些函数式语言了。用法举例如下:
1
2
ini复制代码List<Integer> list = Stream.of(2,2,3,4,5,6,7,8,9,10)  
    .takeWhile(i -> (i % 2 == 0)).collect(Collectors.toList());

本文转载自: 掘金

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

C++ 11 实现(同步的)任务队列线程池 正文

发表于 2021-11-08

「这是我参与11月更文挑战的第 7 天,活动详情查看:2021最后一次更文挑战」。

参加该活动的第 15 篇文章

参考原文地址: (原创)C++ 半同步半异步线程池

我对文章的格式和错别字进行了调整,并在他的基础上,根据我自己的理解把重点部分进一步解释完善(原作者的代码注释甚少)。以下是正文。

正文

线程池可以开启多个线程高效并行处理任务,一开始各个线程会等待同步队列中的任务到来,任务到来后多个线程会抢着执行,但是当到来的任务太多并且达到上限时,线程则需要等待片刻,任务上限是为了保证内存不会溢出。

image.png
线程池的效率和 CPU 核数相关,多核的话效率会更高,线程数一般取 CPU 数量+ 2 比较合适,否则线程过多,线程频繁切换反而会导致效率降低。

线程池有两个活动过程:

  1. 外面有一个线程不停的为线程池(的任务队列)添加任务;
  2. 线程池内部的线程不停地(从任务队列中)取任务执行。

活动图如下

image.png

线程池中的队列是用的上一篇博文中的同步队列。具体代码:

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
cpp复制代码#include <vector>
#include <thread>
#include <functional>
#include <memory>
#include <atomic>
#include "SyncQueue.hpp"

const int MaxTaskCount = 100;
class ThreadPool
{
public:
using Task = std::function<void()>;

/// @note 获取硬件支持的并发数
ThreadPool(int numThreads = std::thread::hardware_concurrency()) : m_queue(MaxTaskCount)
{
Start(numThreads);
}

~ThreadPool(void)
{
/// @note 主动停止线程池
Stop();
}

/// @note 关闭线程池
void Stop()
{
std::call_once(m_flag, [this]
{ StopThreadGroup(); }); ///< 保证多线程情况下只调用一次 StopThreadGroup
}

/// @note 为任务队列添加任务
void AddTask(Task &&task)
{
m_queue.Put(std::forward<Task>(task));
}

/// @note 同上
void AddTask(const Task &task)
{
m_queue.Put(task);
}

private:
/// @note 开启线程池
void Start(int numThreads)
{
m_running = true;
/// @note 创建线程组(std::list<std::shared_ptr<std::thread>>)
for (int i = 0; i < numThreads; ++i)
{
m_threadgroup.push_back(std::make_shared<std::thread>(&ThreadPool::RunInThread, this));
}
}

/// @note 各个线程从任务队列中取出任务,然后执行
void RunInThread()
{
while (m_running)
{
/// @note 取任务分别执行
std::list<Task> list;
m_queue.Take(list);

for (auto &task : list)
{
if (!m_running)
return;

task();
}
}
}

/// @note Stop 调用,关闭线程池
void StopThreadGroup()
{
m_queue.Stop(); ///< 任务队列的出队和入队操作中断
m_running = false; ///< 置为 false ,让内部线程跳出循环并退出

for (auto thread : m_threadgroup) ///< 等待线程池的线程结束
{
if (thread)
thread->join();
}
m_threadgroup.clear();
}

std::list<std::shared_ptr<std::thread>> m_threadgroup; ///< 处理任务的线程组
SyncQueue<Task> m_queue; ///< 线程同步的任务队列
atomic_bool m_running; ///< 是否停止的标志
std::once_flag m_flag; ///< 用于 std::call_once
};

上面的代码中用到了同步队列 SyncQueue ,它的实现在这里。测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
cpp复制代码void TestThdPool()
{
ThreadPool pool;
bool runing = true;

/// @note 不停地给线程池添加任务的子线程
std::thread thd1([&pool, &runing]
{
while (runing)
{
cout << "produce " << this_thread::get_id() << endl;

pool.AddTask([]
{ std::cout << "consume " << this_thread::get_id() << endl; });
}
});

this_thread::sleep_for(std::chrono::seconds(10));
runing = false;
pool.Stop();

thd1.join();
getchar();
}

上面的测试代码中,thd1 是生产者线程,线程池内部会不断消费生产者产生的任务。在需要的时候可以提前停止线程池,只要调用 Stop 函数就行了。

执行结果如下

image.png

本例中涉及的其他知识点

std::call_once

1
2
3
4
> cpp复制代码template <class Fn, class... Args>
> void call_once (once_flag& flag, Fn&& fn, Args&&...args);
>
>
  • 第一个参数是 std::once_flag 的对象( once_flag 是不允许修改的,其拷贝构造函数和 operator= 函数都声明为 delete ),
  • 第二个参数可调用实体,即要求只执行一次的代码,后面可变参数是其参数列表。
  • call_once 保证函数 fn 只被执行一次,如果有多个线程同时执行函数 fn 调用,则只有一个活动线程(active call)会执行函数,其他的线程在这个线程执行返回之前会处于 ”passive execution” (被动执行状态) —— 不会直接返回,直到活动线程对 fn 调用结束才返回。对于所有调用函数 fn 的并发线程,数据可见性都是同步的(一致的)。
  • 如果活动线程在执行 fn 时抛出异常,则会从处于 ”passive execution” 状态的线程中挑一个线程成为活动线程继续执行 fn ,依此类推。一旦活动线程返回,所有 ”passive execution” 状态的线程也返回, 不会成为活动线程。(实际上 once_flag 相当于一个锁,使用它的线程都会在上面等待,只有一个线程允许执行。如果该线程抛出异常,那么从等待中的线程中选择一个,重复上面的流程)。

std::thread::hardware_concurrency

  • 功能,获取硬件支持的并发线程数
  • 返回值,正常返回支持的并发线程数,若值非错误定义或不可计算,则返回 0

本文转载自: 掘金

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

SpringBoot单元测试

发表于 2021-11-08

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」

SpringBoot提供的单元测试功能让测试更简单方便、无需关注整个业务流程。

  1. 单元测试的灵魂三问

1.1 是什么

单元测试就是对项目中最小单元进行功能可行测试,Java中的最小单元即一个类、方法、代码片段。

1.2 为什么

项目开发过程中想要验证开发的模块是否实现了相应功能,如果启动整个项目进行测试,不仅速度慢耗时久,并且验证流程需要从头开始,大大增加了验证成本。

而单元测试只需要关注代码中某个单元,输入参数后执行并返回结果,验证结果的正确性,以此来快速高效的测试代码功能。

使用JUnit单元测试时,可以使用其中提供的断言来验证期望值,便于查看测试结果。

1.3 怎么用

使用IDEA新建项目时如果使用spring initialize选项构建项目,则项目创建之后便会自动携带单元测试的相关依赖spring-boot-starter-test,项目的最基本依赖有两个,另一个是spring-boot-starter。

如果项目中没有引入单元测试依赖,还可以手动添加引入:

1
2
3
4
5
xml复制代码 <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
 </dependency>

依赖引入之后,使用单元测试的条件已经具备,就可以对代码单元进行测试了,在单元测试的使用过程中,需要注意:

  1. 单元测试要简单明了,专心于某一项功能进行验证
  2. 每个单元测试独立存在,需要单独进行验证
  3. 针对每个功能的单元测试要覆盖成功、失败等多种情况
  1. 单元测试的创建

2.1 单元测试约定

SpringBoot项目中的测试类默认放于项目的src/test包中,对比src/main包中项目实际结构来创建对应测试类,测试创建常用规则:

  • 测试类的包结构和项目相同,且为项目中每个类提供一个对应测试类
  • 测试类一般命名为xxTest的形式,保证直观可认,并使用@SpringBootTest注解标注
  • 测试类中的方法命名以test开头,并使用@Test注解标注

2.2 常用注解

SpringBoot中使用注解来完成单元测试的相关功能

  • @SpringBootTest:标注一个类作为测试类,为SpringApplication创建上下文名支持SpringBoot
  • @BeforeEach:在测试方法执行之前需要执行的方法
  • @Test:注解标注一个方法为测试方法,注解中可以设置时间参数,代表方法测试超时时间
  • @Transactional:声明事务管理
  • @Rollback:设置值为true/false,指定是否数据回滚来保证测试数据不污染数据库
  1. 不同层的单元测试

由于SpringBoot项目多是MVC分层结构,开发过程中对不同层结构都需要实现一定的功能,可以在完成当前层功能后进行单元测试

3.1 Mapper(Dao)层

Mapper层是直接操作数据库的层级,进行单元测试时,为避免测试数据污染数据库,可以对测试方法使用@Transactional和@Rollback(vaule=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
25
java复制代码//Junit5写法
@SpringBootTest
public class MyBatisTestMapperTest {
@Autowired
private MybatisTestMapper mybatisTestMapper;

@Test
public void testQuery(){
MybatisTest mybatisTest = mybatisTestMapper.queryById(1);
Assertions.assertNotNull(mybatisTest);
Assertions.assertEquals("tom",mybatisTest.getName(),"false");
}

@Test
@Transactional
@Rollback
public void testInsert(){
MybatisTest mybatisTest = new MybatisTest();
mybatisTest.setName("Lily");
mybatisTest.setPhone("10011");
mybatisTest.setEmail("1234455@qq.com");
int n = mybatisTestMapper.insert(mybatisTest);
Assertions.assertEquals(1,n);
}
}

3.2 Service层

Service作为Mapper的上层,在Service中调用了Mapper方法来操作数据库,使用时同样只需要注入Service层的bean,然后直接调用其中功能方法即可。

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复制代码//JUnit5写法
@SpringBootTest
public class MyBatisTestServiceTest {
@Autowired
private MybatisTestService mybatisTestService;

@Test
public void testQueryById(){
Assertions.assertNotNull(mybatisTestService.queryById(1));
Assertions.assertEquals("tom",mybatisTestService.queryById(1).getName(),"false");
}

@Test
@Transactional
@Rollback
public void testInsert(){
MybatisTest mybatisTest = new MybatisTest();
mybatisTest.setName("Liar");
mybatisTest.setPhone("1567890");
mybatisTest.setEmail("1100099@163.com");
int n = mybatisTestService.insert(mybatisTest);
Assertions.assertEquals(1,n,"false");
}
}

注意,如果Service层测试时引用了Mapper层文件并操作了数据库,为避免污染数据,也需要使用@Transactional和@Rollback注解。

3.3 Controller层

Controller层作为网络接口层,需要进行web请求测试,这就需要使用Spring测试框架提供的MockMvc对象。
执行测试方法之前,需要创建mockMvc对象来执行网络请求,可以使用@Before注解实现初始化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码//JUnit5
@SpringBootTest
public class MybatisTestControllerTest {

@Autowired
private MybatisTestController mybatisTestController;

private MockMvc mockMvc;

@BeforeEach
public void initMock(){
mockMvc = MockMvcBuilders.standaloneSetup(mybatisTestController).build();
}

@Test
public void testGetMybatisInfo() throws Exception {
String url = "/get";
MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get(url))
.andExpect(MockMvcResultMatchers.status().isOk())
.andReturn();
Assertions.assertNotNull(result);
}
}

本文转载自: 掘金

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

SpringBoot——run方法启动过程源码分析 项目启动

发表于 2021-11-08

项目启动入口

Image [3].png

实际执行的内容是通过SpringApplication类的静态方法创建一个ConfigurableApplicationContext,顾名思义,即可配置的对象容器,也就是Springboot中的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified source using default settings.
* @param primarySource the primary source to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
* primarySource为Springboot启动类,args为启动参数
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}

/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}

SpringApplication类

Image [6].png

SpringApplication主要的构造函数及初始化过程

在初始化的时候会初始化他的成员变量,成员变量会先取默认值,以及加载初始化器和监听器

Image [8].png

Image [7].png
加载的方式都是通过getSpringFactoriesInstances方法调用loadSpringFactories从”META-INF/spring.factories”文件读入要加载的类的全路径类型,获取到全路径类名之后,通过如下代码,根据全路径类名通过反射的方式创建相应的bean:

Image [9].png
下面这个方法就是通过反射来创建bean实例了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes,
ClassLoader classLoader, Object[] args, Set<String> names) {
List<T> instances = new ArrayList<>(names.size());
for (String name : names) {
try {
//看到这个forName应该有点熟悉吧,没错,就是通过反射的方式加载实例化bean的
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
Assert.isAssignable(type, instanceClass);
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
T instance = (T) BeanUtils.instantiateClass(constructor, args);
instances.add(instance);
}
catch (Throwable ex) {
throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
}
}
return instances;
}

在通过构造函数创建SpringApplication类的实例时,会先加载一部分能够加载的资源

SpringApplication对象的run方法

先附上这个方法的完整代码,通过注释可以知道这个方法的作用就是创建和刷新一个新的ApplicationContext,也就是Springboot的上下文bean容器

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
java复制代码/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
//1.StopWatch为一个简单地计时器,记录Springboot应用启动的时间
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
//设置java.awt.headless系统属性为true,Headless模式是系统的一种配置模式。
// 在该模式下,系统缺少了显示设备、键盘或鼠标。但是服务器生成的数据需要提供给显示设备等使用。
// 因此使用headless模式,一般是在程序开始激活headless模式,告诉程序,现在你要工作在Headless mode下,依靠系统的计算能力模拟出这些特性来
configureHeadlessProperty();
//2.获取监听器集合对象
SpringApplicationRunListeners listeners = getRunListeners(args);
//发出开始执行的starting事件
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
//3.根据SpringApplicationRunListeners以及参数来准备环境,这一步会发出environmentPrepared事件
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
//打印banner就是启动项目时打印的图案
Banner printedBanner = printBanner(environment);
4.根据WebApplicationType创建ApplicationContext容器
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
//5.初始化ApplicationContext,这一步会先后发布contextPrepared和contextLoaded两个事件
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//刷新context
refreshContext(context);
//没有逻辑
afterRefresh(context, applicationArguments);
//计时结束
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//发布started事件
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
//发布running事件
listeners.running(context);
}
catch (Throwable ex) {
//如果失败,会发布failed事件
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

1.创建计时器开始计时

StopWatch会记录项目开始启动到启动完毕的时间,我们在启动项目的时候日志里面会有一行日志输出启动时间就是通过StopWatch实现的

Image [10].png

2.加载SpringApplicationRunListeners

首先第一步是:通过SpringFactoriesLoader 到META-INF/spring.factories查找并加载所有的SpringApplicationRunListeners,通过start()方法通知所有的SpringApplicationRunListener,本质上这是一个事件发布者,他在SpringBoot应用启动的不同阶段会发布不同的事件类型。
SpringApplicationRunListener接口只有一个实现类EventPublishingRunListener,也就是说SpringApplicationRunListeners类的List listeners中只会生成一个EventPublishingRunListener实例。那么SpringApplicationRunListener是如何发布事件类型的呢?首先我们看下SpringApplicationRunListener这个接口。接口每个方法的注释上面都将其调用的时机交代得很清楚
SpringApplicationRunListener监听器SpringBoot应用启动的不同阶段都会有相应的监听通知。通知贯穿了SpringBoot应用启动的完成过程

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
java复制代码public interface SpringApplicationRunListener {

/**run方法刚执行时通知
* Called immediately when the run method has first started. Can be used for very
* early initialization.
* @param bootstrapContext the bootstrap context
*/
default void starting(ConfigurableBootstrapContext bootstrapContext) {
starting();
}


/**Environment准备好,ApplicationContext被创建好之前通知
* Called once the environment has been prepared, but before the
* {@link ApplicationContext} has been created.
* @param bootstrapContext the bootstrap context
* @param environment the environment
*/
default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
ConfigurableEnvironment environment) {
environmentPrepared(environment);
}


/**ApplicationContext被准备好之后,但是sources没有被加载之前通知
* Called once the {@link ApplicationContext} has been created and prepared, but
* before sources have been loaded.
* @param context the application context
*/
default void contextPrepared(ConfigurableApplicationContext context) {
}

/**ApplicationContext被加载好之后,但是没有刷新之前通知
* Called once the application context has been loaded but before it has been
* refreshed.
* @param context the application context
*/
default void contextLoaded(ConfigurableApplicationContext context) {
}

/**
* The context has been refreshed and the application has started but
* {@link CommandLineRunner CommandLineRunners} and {@link ApplicationRunner
* ApplicationRunners} have not been called.
* @param context the application context.
* @since 2.0.0
*/
default void started(ConfigurableApplicationContext context) {
}

/**run方法结束之前并且所有的application context都被加载
* Called immediately before the run method finishes, when the application context has
* been refreshed and all {@link CommandLineRunner CommandLineRunners} and
* {@link ApplicationRunner ApplicationRunners} have been called.
* @param context the application context.
* @since 2.0.0
*/
default void running(ConfigurableApplicationContext context) {
}

/**运行application失败
* Called when a failure occurs when running the application.
* @param context the application context or {@code null} if a failure occurred before
* the context was created
* @param exception the failure
* @since 2.0.0
*/
default void failed(ConfigurableApplicationContext context, Throwable exception) {
}

3.创建并配置当前应用将要使用的环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
scss复制代码private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
// 获取创建的环境,如果没有则创建,如果是web环境则创建StandardServletEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置Environment:配置profile以及properties
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

创建并配置当前应用的环境(Environment),Environment用于描述应用程序当前的运行环境,其抽象了两方面的内容:
配置文件(profile)和属性(properties),我们知道不同的环境(开发环境,测试环境,发布环境)可以使用不同的属性配置,这些属性配置可以从配置文件,环境变量,命令行参数等来源获取。因此,当Environment准备好之后,在整个应用的任何时候,都可以获取这些属性。

所以,这一步的做的事情主要有三件:

  1. 获取创建的环境(Environment),如果没有则创建,如果是web环境则创建StandardServletEnvironment,如果不是的话则创建StandardEnvironment。
  2. 配置环境(Environment):主要是配置profile和属性properties
  3. 调用SpringApplicationRunListener的environmentPrepared方法,通知事件监听者:应用环境(Environment)已经准备好了。

4.根据WebApplicationType创建ApplicationContext容器

最终的创建代码是一段lambda表达式,根据对应的webApplicationType创建ApplicationContextFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};

5.初始化ApplicationContext

前面个步骤已经创建好了与本应用环境相匹配的ApplicationContext实例,那么接下来就是对ApplicationContext进行初始化了。这一步也是比较核心的一步。首先让我们来看看实现逻辑的相关代码:

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复制代码private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
//1. 将准备好的Environment设置给ApplicationContext
context.setEnvironment(environment);
postProcessApplicationContext(context);
//2. 遍历调用所有的ApplicationContextInitializer的 initialize() 方法来对已经创建好的 ApplicationContext 进行进一步的处理。
applyInitializers(context);
//3. 调用SpringApplicationRunListeners的 contextPrepared() 方法,通知所有的监听者,ApplicationContext已经准备完毕
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
//4. 将applicationArguments实例注入到IOC容器
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
//5. 将printedBanner实例注入到IOC容器
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
//6. 加载资源,这里的资源一般是启动类xxxApplication
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
//7. 将所有的bean加载到容器中
load(context, sources.toArray(new Object[0]));
//8.通知所有的监听者:ApplicationContext已经装载完毕
listeners.contextLoaded(context);
}

以上就是初始化ApplicationContext的主要逻辑,主要有如下逻辑:

  1. 将准备好的Environment设置给ApplicationContext
  2. 遍历调用所有的ApplicationContextInitializer的 initialize() 方法来对已经创建好的 ApplicationContext 进行进一步的处理
  3. 调用SpringApplicationRunListeners的 contextPrepared() 方法,通知所有的监听者,ApplicationContext已经准备完毕
  4. 将applicationArguments实例注入到IOC容器。
  5. 将printedBanner实例注入到IOC容器,这个就是之前生成的Banner的实例。
  6. 加载资源,这里的资源一般是启动类xxxApplication
  7. 将所有的bean加载到容器中
  8. 通知所有的监听者:ApplicationContext已经装载完毕。

6.调用ApplicationContext的refresh() 方法

上下文这个步骤将会解析xml配置以及java配置,从而把Bean的配置解析成为BeanDefinition,将从而把Bean的配置解析成为BeanDefinition加载到上下文容器。这里的 SpringApplication的 refresh方法最终还是调用到AbstractApplicationContext的refresh方法。
AbstractApplicationContext的refresh方法:

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
java复制代码@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 刷新前准备,设置flag、时间,初始化properties等
prepareRefresh();

// 获取ApplicationContext中组合的BeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 设置类加载器,添加后置处理器等准备
prepareBeanFactory(beanFactory);

try {
// 供子类实现的后置处理
postProcessBeanFactory(beanFactory);

// 调用Bean工厂的后置处理器,实际的bean初始化和加载都在这一步完成
invokeBeanFactoryPostProcessors(beanFactory);

// 注册Bean的后置处理器
registerBeanPostProcessors(beanFactory);

// 初始化消息源
initMessageSource();

// 初始化事件广播
initApplicationEventMulticaster();

// 供之类实现的,初始化特殊的Bean
onRefresh();

// 注册监听器
registerListeners();

// 实例化所有的(non-lazy-init)单例Bean
finishBeanFactoryInitialization(beanFactory);

// 发布刷新完毕事件
finishRefresh();
}

catch (BeansException ex) {
//
} finally {
//
}
}
}

最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext。ioc容器的refresh过程先做一个小结。我们知道了上下文和Bean容器是继承关系又是组合关系。refreshContext的核心就是为了加载BeanDefinition,而加载BeanDefinition将从main方法所在的主类开始,主类作为一个配置类将由ConfigurationClassParser解析器来完成解析的职责

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

本文转载自: 掘金

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

【死磕NIO】— NIO基础详解 缓冲区 Buffer 通道

发表于 2021-11-08

「这是我参与11月更文挑战的第 1 天,活动详情查看:2021最后一次更文挑战


Netty 是基于Java NIO 封装的网络通讯框架,只有充分理解了 Java NIO 才能理解好Netty的底层设计。Java NIO 由三个核心组件组件:

  • Buffer
  • Channel
  • Selector

缓冲区 Buffer

Buffer 是一个数据对象,我们可以把它理解为固定数量的数据的容器,它包含一些要写入或者读出的数据。

在 Java NIO 中,任何时候访问 NIO 中的数据,都需要通过缓冲区(Buffer)进行操作。读取数据时,直接从缓冲区中读取,写入数据时,写入至缓冲区。NIO 最常用的缓冲区则是 ByteBuffer。下图是 Buffer 继承关系图:

每一个 Java 基本类型都对应着一种 Buffer,他们都包含这相同的操作,只不过是所处理的数据类型不同而已。

通道 Channel

Channel 是一个通道,它就像自来水管一样,网络数据通过 Channel 这根水管读取和写入。传统的 IO 是基于流进行操作的,Channle 和类似,但又有些不同:

区别 流 通过Channel
支持异步 不支持 支持
是否可双向传输数据 不能,只能单向 可以,既可以从通道读取数据,也可以向通道写入数据
是否结合 Buffer 使用 不 必须结合 Buffer 使用
性能 较低 较高

正如上面说到的,Channel 必须要配合 Buffer 一起使用,我们永远不可能将数据直接写入到 Channel 中,同样也不可能直接从 Channel 中读取数据。都是通过从 Channel 读取数据到 Buffer 中或者从 Buffer 写入数据到 Channel 中,如下:

简单点说,Channel 是数据的源头或者数据的目的地,用于向 buffer 提供数据或者读取 buffer 数据,并且对 I/O 提供异步支持。

下图是 Channel 的类图

Channel 为最顶层接口,所有子 Channel 都实现了该接口,它主要用于 I/O 操作的连接。定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码public interface Channel extends Closeable {

/**

* 判断此通道是否处于打开状态。

*/

public boolean isOpen();

/**

*关闭此通道。

*/

public void close() throws IOException;
}

最为重要的Channel实现类为:

  • FileChannel:一个用来写、读、映射和操作文件的通道
  • DatagramChannel:能通过 UDP 读写网络中的数据
  • SocketChannel: 能通过 TCP 读写网络中的数据
  • ServerSocketChannel:可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel

多路复用器 Selector

多路复用器 Selector,它是 Java NIO 编程的基础,它提供了选择已经就绪的任务的能力。从底层来看,Selector 提供了询问通道是否已经准备好执行每个 I/O 操作的能力。简单来讲,Selector 会不断地轮询注册在其上的 Channel,如果某个 Channel 上面发生了读或者写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。

Selector 允许一个线程处理多个 Channel ,也就是说只要一个线程复杂 Selector 的轮询,就可以处理成千上万个 Channel ,相比于多线程来处理势必会减少线程的上下文切换问题。下图是一个 Selector 连接三个 Channel :

实例

服务端

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
ini复制代码public class NIOServer {
/*接受数据缓冲区*/

private ByteBuffer sendbuffer = ByteBuffer.allocate(1024);

/*发送数据缓冲区*/

private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);

private Selector selector;

public NIOServer(int port) throws IOException {

// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);

// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();

// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));

// 通过open()方法找到Selector
selector = Selector.open();

// 注册到selector,等待连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

System.out.println("Server Start----:");

}

private void listen() throws IOException {

while (true) {

selector.select();

Set<SelectionKey> selectionKeys = selector.selectedKeys();

Iterator<SelectionKey> iterator = selectionKeys.iterator();

while (iterator.hasNext()) {

SelectionKey selectionKey = iterator.next();

iterator.remove();

handleKey(selectionKey);

}

}

}

private void handleKey(SelectionKey selectionKey) throws IOException {

// 接受请求
ServerSocketChannel server = null;

SocketChannel client = null;

String receiveText;

String sendText;

int count=0;

// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {

// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();

// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();

// 配置为非阻塞
client.configureBlocking(false);

// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);

} else if (selectionKey.isReadable()) {

// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();

//将缓冲区清空以备下次读取
receivebuffer.clear();

//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);

if (count > 0) {

receiveText = new String( receivebuffer.array(),0,count);

System.out.println("服务器端接受客户端数据--:"+receiveText);

client.register(selector, SelectionKey.OP_WRITE);

}

} else if (selectionKey.isWritable()) {

//将缓冲区清空以备下次写入
sendbuffer.clear();

// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();

sendText="message from server--";

//向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());

//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();

//输出到通道
client.write(sendbuffer);

System.out.println("服务器端向客户端发送数据--:"+sendText);

client.register(selector, SelectionKey.OP_READ);

}

}

public static void main(String[] args) throws IOException {

int port = 8080;

NIOServer server = new NIOServer(port);

server.listen();

}

}

客户端

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
ini复制代码public class NIOClient {
/*接受数据缓冲区*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(1024);

/*发送数据缓冲区*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(1024);

public static void main(String[] args) throws IOException {

// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();

// 设置为非阻塞方式
socketChannel.configureBlocking(false);

// 打开选择器
Selector selector = Selector.open();

// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);

// 连接

socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));

Set<SelectionKey> selectionKeys;

Iterator<SelectionKey> iterator;

SelectionKey selectionKey;

SocketChannel client;

String receiveText;

String sendText;

int count=0;

while (true) {

//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();

//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();

//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();

while (iterator.hasNext()) {

selectionKey = iterator.next();

if (selectionKey.isConnectable()) {

System.out.println("client connect");

client = (SocketChannel) selectionKey.channel();

// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {

client.finishConnect();

System.out.println("完成连接!");

sendbuffer.clear();

sendbuffer.put("Hello,Server".getBytes());

sendbuffer.flip();

client.write(sendbuffer);

}

client.register(selector, SelectionKey.OP_READ);

} else if (selectionKey.isReadable()) {

client = (SocketChannel) selectionKey.channel();

//将缓冲区清空以备下次读取
receivebuffer.clear();

//读取服务器发送来的数据到缓冲区中
count=client.read(receivebuffer);

if(count>0){

receiveText = new String( receivebuffer.array(),0,count);

System.out.println("客户端接受服务器端数据--:"+receiveText);

client.register(selector, SelectionKey.OP_WRITE);

}

} else if (selectionKey.isWritable()) {

sendbuffer.clear();

client = (SocketChannel) selectionKey.channel();

sendText = "message from client--";

sendbuffer.put(sendText.getBytes());

//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();

client.write(sendbuffer);

System.out.println("客户端向服务器端发送数据--:"+sendText);

client.register(selector, SelectionKey.OP_READ);

}

}

selectionKeys.clear();

}

}
}

运行结果

本文转载自: 掘金

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

golang 实现 ldif 数据转成 json 初探

发表于 2021-11-08

「这是我参与11月更文挑战的第 8 天,活动详情查看:2021最后一次更文挑战」

上一篇我们分享了如何将 ldif 格式的数据,转换成 json 数据的思路并画相应的简图

这一次,我们就来实现一下

实现方式如下:

  • 连接服务器,查询 ldap 服务器上数据结构 ,goalng 如何获取 ldap 服务器的数据? 有说到
  • 遍历每一条 entry
  • 处理每一条 entry 的时候,从右到左获取相应的 rdn(对应的键和值),并给每一个 rdn 创建一个 多叉树的 节点
  • basedn 对应的节点 和 每一个 ou 对应的节点地址,存放到一个 map(key 是 string,value 是节点的地址) 中便于后续遍历处理其他 entry 的时候,直接通过 ou 名字获取对应节点地址即可
  • 对于一个节点下面的用户,直接挂到这个节点上即可

来一起看看数据结构和 main 函数

数据结构为节点的必要信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// 节点信息
type lNode struct {
Name string
Path string
Children []*lNode
User []string
}
// 新建一个节点
func NewNode(name, path string) *lNode {
return &lNode{
Name: name,
Path: path,
Children: []*lNode{},
User: []string{},
}
}

main 函数的执行流程具体如下:

  • 连接ldap 服务器并查询对应数据
  • 处理数据并生成一颗树 (默认 dc 为 根节点, / )
  • 将树转成 json 格式,进行打印输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码func main() {
data := connectLdap(
"ldap://xxxx",
"dc=xiaomotong,dc=com",
"cn=admin,dc=xiaomotong,dc=com",
"123123",
"(&(objectClass=*))")
if len(data) <= 0 {
fmt.Println("search no data !!")
}
mp := make(map[string]*lNode)
root := NewNode("dc=xiaomotong,dc=com", "/")
mp["dc=xiaomotong,dc=com"] = root
// 生成一颗树
CreateLdapTree(mp, data, "dc=xiaomotong,dc=com")

b, err := json.Marshal(root)
if err != nil {
fmt.Println("json.Marshal error !!!")
return
}
fmt.Println(string(b))
}

从 ldap 服务器上获取数据

我们简单就在 一个 main.go 文件中实现一下,代码结构是这样的

func connectLdap(addr, baseDB, username, passwd, filter string) []*ldap.Entry { 函数的具体实现,在文章 goalng 如何获取 ldap 服务器的数据? 有体现,我们这一次只是将参数调整了一下

处理 ldap 响应的数据

ldap 返回的数据是以 ldif 格式返回的,会返回0条到多条 entry,我们需要逐个的来解析每一个 entry 里面的数据

一个 entry 就是一个 DN ,一个 DN 里面有多个 RDN,一个 RDN 就是一个键值对

  • 创建根节点,信息是 BASEDN :dc=xiaomotong,dc=com , 并将信息放到 map 中

  • 开始解析数据每一条 dn,dn 中的 每一个 rdn 创建对应的节点,并通过dn 从右到左的顺序,将 rdn 连接起来
  • 一个组里面有子组,就放在 node 的 Children 里面, 一个组里面的 用户就放在 User里面,当前节点的名字 放在 name中,当前节点的绝对路径就放在 path 中

来看看 func CreateLdapTree(mp map[string]*lNode, Entries []*ldap.Entry, BASEDN string) { 函数

1
2
3
4
5
6
7
8
9
10
go复制代码// 创建一棵树
func CreateLdapTree(mp map[string]*lNode, Entries []*ldap.Entry, BASEDN string) {
// 遍历 Entries
for _, Entry := range Entries {
if BASEDN == Entry.DN {
continue
}
ProcessDN(Entry.DN, mp, BASEDN)
}
}

CreateLdapTree 里面具体的实现是遍历 ldap 的所有 entry,并调用 ProcessDN 函数来解析 dn 数据,且根据 dn 来生成对应的多叉树片段

具体处理 DN 数据

func ProcessDN(DN string, mp map[string]*lNode, BASEDN string) { 是具体处理 DN 数据的主要函数

  • 主要做的是解析一条 DN 数据,并生成一个多叉树的片段
  • ou 的节点地址会相应放到 map 中进行记录,便于后续使用

处理的逻辑,会去判断 rdn 的 key 是 dc,cn,ou,来做相应的处理,如果是 ou 就创建节点,并将节点的地址记录在 map 中

json 序列化

最后将数据结构序列化成 json,并以字符串的方式打印出来

上述代码逻辑也比较简单,就是将 ldif 转成树而已,代码流程是

整个 main.go 文件,执行之后,结果如下,成功将 ldif 转成多叉树,且已 json 的方式展现出来

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
json复制代码{
"Name": "dc=xiaomotong,dc=com",
"Path": "/",
"Children": [
{
"Name": "People",
"Path": "/People/",
"Children": [],
"User": [
"xiaozhupeiqi"
]
},
{
"Name": "dev",
"Path": "/dev/",
"Children": [
{
"Name": "golang",
"Path": "/dev/golang/",
"Children": [],
"User": [
"xiaoppp"
]
},
{
"Name": "clang",
"Path": "/dev/clang/",
"Children": [
{
"Name": "woshixiaozhu",
"Path": "/dev/clang/woshixiaozhu/",
"Children": [],
"User": [
"xiaopang2"
]
}
],
"User": []
},
{
"Name": "java",
"Path": "/dev/java/",
"Children": [],
"User": []
}
],
"User": []
}
],
"User": [
"admin",
"zhangsan",
"xiaopang",
"xiaopang2"
]
}

学习所得,如有偏差,还请不吝赐教,细心的朋友会发现上述逻辑有坑,下次见

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是小魔童哪吒,欢迎点赞关注收藏,下次见~

本文转载自: 掘金

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

JVM调优排错(解析&实际案例)

发表于 2021-11-08

前言

本案例只是简单介绍JVM排查工具,调优措施,以及普遍示例简单解析。(更完整命令可以参考:docs.oracle.com/javase/7/do…)

一. 常用工具

  1. jmap

jmap命令是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的,所有‘对象’的情况(如:产生那些对象,及其数量)。
使用示例:

  1. jmap -histo $PID –> 显示堆中对象的统计信息
    image.png

其中instance表示元素出现的个数,bytes表示元素所占内存大小(单位kb)

  1. jamp -heap $PID –> 显示Java堆详细信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
js复制代码Attaching to process ID 31370, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.45-b02

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 492830720 (470.0MB)
NewSize = 10485760 (10.0MB)
MaxNewSize = 164102144 (156.5MB)
OldSize = 20971520 (20.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 7864320 (7.5MB)
used = 5038400 (4.80499267578125MB)
free = 2825920 (2.69500732421875MB)
64.06656901041667% used
From Space:
capacity = 5767168 (5.5MB)
used = 131072 (0.125MB)
free = 5636096 (5.375MB)
2.272727272727273% used
To Space:
capacity = 6291456 (6.0MB)
used = 0 (0.0MB)
free = 6291456 (6.0MB)
0.0% used
PS Old Generation
capacity = 117964800 (112.5MB)
used = 78530512 (74.89253234863281MB)
free = 39434288 (37.60746765136719MB)
66.57113986545139% used
  1. jmap -dump:live,format=b,file=dumpfile $PID –> 生成堆转储快照dump文件

或jmap -dump:live,format=b,file=heapdump.hprof $PID,hprof方便通过mat解析。live子选项是可选的。如果指定了live子选项,堆中只有活动的对象会被转储。(执行较为耗时)

  1. jstack

jstack是java虚拟机自带的一种堆栈跟踪工具。常用于分析线程问题(如线程间死锁[deadLock])
使用示例:

  1. jstack -l $PID > /tmp/dump/jstack.log –> 导出堆栈信息

-l :会打印出额外的锁信息
image.png

  1. 找出java进程中消耗CPU最多的线程TID
  • 结合top命令可以查出占用CPU最高的线程: top -H -p pid

详细输出可以使用:ps p {pid}-L -o pcpu,pid,tid,time,tname,cmd

  • 将TID转化成十六进制: printf 0x%x n
  • 从jstack.log 文件中找出TID对应的16进制数据
  1. jstat

JVM statistics Monitoring,用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译(即使编译)等运行数据。

  1. jstat -gc $PID –> 获取垃圾回收等信息

image.png

1
2
3
4
5
6
7
8
9
10
js复制代码 结果分析:(C一般是容量,U结尾一般是使用量)
堆内存 = 年轻代 + 年老代 + 永久代
年轻代 = Eden区 + 两个Survivor区
S0C、S1C、S0U、S1U:Survivor 0/1区容量(Capacity)和使用量(Used)
EC、EU:Eden区容量和使用量
OC、OU:年老代容量和使用量
PC、PU:永久代容量和使用量
YGC、YGT:年轻代GC次数和GC耗时
FGC、FGCT:Full GC次数和Full GC耗时
GCT:GC总耗时

clipboard.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
js复制代码引言:JVM配置中除了常用的xmx,xms
-Xmn、-XX:NewRatio、-XX:SurvivorRatio:

- -Xmn

**设置新生代大小**

- -XX:NewRatio

    新生代(eden+2*s)和老年代(不包含永久区)的比值

        例如:4,表示新生代:老年代=1:4,即新生代占整个堆的1/5

- -XX:SurvivorRatio(幸存代)

    设置两个Survivor区和eden的比值

        例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
  1. GC结果分析
    ygc平均耗时=YGCT/YGC(s)=5176.81/469204=0.011s=11ms
    ygc时间间隔=YGC/程序的运行时间
1
2
3
4
5
6
js复制代码如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;
如果GC时间超过1〜3 秒,或者频繁G C ,则必须优化。如果满足下面的指标,则一般不需要进行GC;
■ Minor GC执行时间不到50ms;\
■ Minor GC执行不频繁,约10秒一次;\
■ Full GC执行时间不到1s;\
■ Full GC执行频率不算频繁,不低于10分钟1次。

本文转载自: 掘金

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

Docker 教程(二):Dockerfile

发表于 2021-11-08

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

本文源自并发编程网的翻译邀请,翻译的是 Jakob Jenkov 的 《Docker 教程》 中的第二篇。

Dockerfile包含一组关于如何构建Docker镜像的说明,通过docker build命令执行Dockerfile文件,可以构建一个Docker镜像,本文介绍了如何编写Dockerfile文件以及构建一个Docker镜像。

Dockerfile的好处

Dockerfile文件以书面形式说明了如何构建一个Docker镜像,Docker镜像通常包含如下内容:

  • 首先需要一个基本的Docker镜像,在这个基础Docker镜像上构建自己的Docker镜像。
  • 一组需要安装在Docker镜像中的工具和应用。
  • 一组需要复制到Docker镜像中的文件(比如配置文件)。
  • 可能需要在防火墙中打开的网络(TPC/UDP)端口或其他。
  • 等等。。。

首先,在Dockerfile文件中以书面形式说明这些,就意味着,我们不用特意记住应用程序如何安装,包括操作系统什么要求、需要安装的应用程序、需要赋值的文件、需要打开的网络端口等,这些内容都被记录在Dockerfile中。

另外,通过Dockerfile文件构建Docker镜像,我们不需要手动执行这些繁琐重复且容易出错的工作。Docker会自动做这些事情,简单、快速、且不容易出错。

第三,我们很容易和其他人分享Dockerfile文件,并且他们可以自己构建Docker镜像。

第四,Dockerfile很容易存储在Git这样的版本控制器中,这样就可以跟踪Dockerfile(服务器、应用配置)的变更记录。版本控制器也可以很容易的让人们协同合作,比如在Dockerfile上,以及分享Dockerfile。

Dockerfile的结构

Dockerfile包含一组指令,每个指令有一个命令和参数组成,类似于命令行可执行文件。下面是一个Dockerfile简单示例:

1
2
3
4
5
6
7
8
bash复制代码# 基础镜像
FROM ubuntu:latest

# 这里可以有更多安装软件和复制文件到镜像中的说明。
COPY    /myapp/target/myapp.jar    /myapp/myapp.jar

# 在Docker容器中执行的命令。
CMD echo Starting Docker Container

Docker基础镜像

Docker镜像是由层组成,每一层都会为最终的Docker镜像添加一些内容。每一个层实际上都是一个单独的Docker镜像,所以说,Docker镜像是由一个或多个层镜像组成,我们可以在其上添加自己的层。

当通过Dockerfile文件指定自己的Docker镜像时,通常是从一个Docker基础镜像开始。这是另一个Docker镜像,可以在其上构建自己的Docker镜像。这个Docker基础镜像本身可能也包含多个层,并且是基于另一个基础镜像构建的。

我们可以使用From命令在Dockerfile文件中指定Docker镜像作为基础镜像,如下节所述。

MAINTAINER

MAINTAINER命令用于说明谁在维护这个Dockerfile文件。比如:

1
css复制代码MAINTAINER   Joe Blocks <joe@blocks.com>

MAINTAINER命令并不常用,因为这类信息在Git存储或其他地方有了。

FROM

FROM命令用于指定Docker基础镜像,如果是从原始Linux镜像开始,可以使用如下命令:

1
2
bash复制代码# 基础镜像
FROM ubuntu:latest

CMD

CMD命令用于指定启动Docker容器是需要执行的命令,该容器是基于此Dockerfile构建的Docker镜像,下面是一些Dockerfile的CMD示例:

1
bash复制代码CMD echo Docker container started.

本例是打印“Docker container started”这行文本。

下一个CMD示例是启动一个java应用:

1
bash复制代码CMD java -cp /myapp/myapp.jar com.jenkov.myapp.MainClass arg1 arg2 arg3

COPY

COPY命令将一个或多个文件从主机(从Dockerfile文件构建Docker镜像的机器)复制到Docker镜像中,可以复制的内容包括文件或目录,下面是一个示例:

1
bash复制代码COPY    /myapp/target/myapp.jar    /myapp/myapp.jar

这个例子是把主机的/myapp/target/myapp.jar文件复制到Docker进行中的/myapp/myapp.jar文件。第一个参数是主机路径(从哪里来),第二个参数是Docker镜像的路径(到哪里去)。

我们还可以复制一个目录到Docker镜像中,比如:

1
arduino复制代码COPY    /myapp/config/prod    /myapp/config

这个例子是把主机的/myapp/config/prod目录复制到Docker镜像中的/myapp/config目录。

我们还可以复制多个文件到Docker镜像中的一个目录中,比如:

1
arduino复制代码COPY    /myapp/config/prod/conf1.cfg   /myapp/config/prod/conf2.cfg   /myapp/config/

这个例子是将主机的/myapp/config/prod/conf1.cfg文件和/myapp/conig/prod/conf2.cfg文件复制到Docker镜像中的/myapp/config/目录中。注意,目标目录必须以/(斜杠)结束才能工作。

ADD

ADD命令与COPY命令工作方式相同,只有一些细微的差别:

  • ADD命令可以复制并提取TAR文件到Docker镜像中。
  • ADD命令可以通过HTTP下载文件,并复制到Docker镜像中。

下是一些示例:

1
bash复制代码ADD    myapp.tar    /myapp/

这个例子是将指定的TAR文件解压缩并提取到Docker镜像的/myapp/目录中。

下面是另一个例子:

1
bash复制代码ADD    http://jenkov.com/myapp.jar    /myapp/

ENV

ENV命令是在Docker镜像中设置环境变量,此环境变量可用于CMD命令在Docker镜像内部启动应用程序。举个例子:

1
复制代码ENV    MY_VAR   123

本例将环境变量MY_VAR设置为值123。

RUN

RUN可以在Docker镜像中执行命令行指令,执行时机是Docker镜像构建过程中,所以RUN命令只会执行一次。RUN命令可用于在Docker镜像中安装应用程序、提取文件或其他命令行功能,这些操作只需要执行一次,以供Docker镜像后续使用。

1
sql复制代码RUN apt-get install some-needed-app

ARG

ARG命令允许定义一个参数,这个参数可以在通过Dockerfile文件构建Docker镜像时,通过命令参数传递给Docker。比如:

1
复制代码ARG tcpPort

当执行docker build命令执行Dockerfile构建Docker镜像时,可以指定tcpPort参数,比如:

1
ini复制代码docker build --build-arg tcpPort=8080 .

注意,--build-arg后面的tcpPort=8080,是将tcpPort参数的值设置为8080。

我们可以通过多个ARG命令定义多个参数,举个例子:

1
2
复制代码ARG tcpPort
ARG useTls

当构建Docker镜像时,必须为所有构建参数提供值。【译者注,1.13版本之前,不提供值会直接报错,1.13版本之后,不提供值不会报错,但是会弹出警告】。举个例子:

1
ini复制代码docker build --build-arg tcpPort=8080 --build-arg useTls=true .

我们可以为ARG设置默认值,当构建Docker镜像时,如果没有指定参数值,将使用默认值。举个例子:

1
2
ini复制代码ARG tcpPort=8080
ARG useTls=true

如果tcpPort和useTls在生成Docker镜像时,都没有设置参数,将使用默认值8080和true。

ARG声明的参数通常在Dockerfile的其他地方引用,比如:

1
2
3
4
ini复制代码ARG tcpPort=8080
ARG useTls=true

CMD start-my-server.sh -port ${tcpPort} -tls ${useTls}

注意:两个引用${tcpPort}和${useTls},引用名是tcpPort和useTls这两个ARG声明的参数。

1
ini复制代码docker build --build-arg tcpPort=8080

WORKDIR

WORKDIR命令指明了Docker镜像中的工作目录,工作目录将对WORKDIR指令之后的所有命令生效,举个例子:

1
bash复制代码WORKDIR    /java/jdk/bin

EXPOSE

EXPOSE命令将对外开放Docker容器中的网络端口,比如,如果Docker容器运行一个web服务器,那么,该web服务器可能需要打开端口80,以便客户端链接到它。举个例子:

1
yaml复制代码EXPOSE   8080

我们还可以指明打开端口的通信协议,比如:UDP和TCP。下面是设置允许通信协议的示例:

1
bash复制代码EXPOSE   8080/tcp 9999/udp

如果没有指定协议,将默认认定为TCP协议。

VOLUME

VOLUME命令会在Docker镜像中创建一个目录,这个目录可以挂载到Docker主机上。换句话说,可以在Docker镜像中创建目录,比如/data,这个目录可以在稍后挂载到Docker主机的/container-data/container1目录上。挂载成功后,容器会启动。下面是一个使用VOLUME命令在Dockerfile中定义装载目录的示例:

1
bash复制代码VOLUME   /data

ENTRYPOINT

ENTRYPOINT命令为从该Docker镜像启动Docker容器提供入口点,入口点是Docker容器启动时执行的应用程序或命令。这样,ENTRYPOINT和CMD工作方式类似,不同之处在于,使用ENTRYPOINT时,当ENTRYPOINT执行的应用程序完成时,Docker容器将关闭。因此,ENTRYPOINT使Docker镜像本身成为一个可执行命令,可以启动,完成后关闭。以下是ENTRYPOINT示例:

1
bash复制代码ENTRYPOINT java -cp /apps/myapp/myapp.jar com.jenkov.myapp.Main

这个示例将在容器启动时执行Java应用程序的主类com.jenkov.myapp.Main,当应用程序关闭时,Docker容器也会关闭。

HEALTHCHECK

HEALTHCHECK命令可以定期执行健康检查,以监视Docker容器中运行的应用程序的运行状况。如果命令返回0,Docker将认为应用程序和容器正常,如果命令返回1,Docker会认为应用程序和容器不正常。示例如下:

1
bash复制代码HEALTHCHECK java -cp /apps/myapp/healthcheck.jar com.jenkov.myapp.HealthCheck https://localhost/healthcheck

这个示例中使用了java应用程序的com.jenkov.myapp.HealthCheck作为健康检查的命令,我们可以使用任何有意义的健康检查命令。

健康检查间隔时间

默认情况下,Docker每30秒执行一次HEALTHCHECK命令。如果想修改时间间隔,我们可以自定义时间,通过--interval参数,可以指定健康检查的检查间隔时间。下面是一个将HEALTHCHECK间隔设置为60秒的示例:

1
bash复制代码HEALTHCHECK --interval=60s java -cp /apps/myapp/healthcheck.jar com.jenkov.myapp.HealthCheck https://localhost/healthcheck

健康检查开始时间

默认情况下,Docker会立即检查Docker容器的监控状况。但是,有些应用程序可能需要一段时间启动,因此,只有经过某段时间后再进行健康检查才有意义。我们可以使用--start-period参数设置健康检查开始时间。下面是一个将健康检查设置为5分钟的示例,在Docker开始健康检查之前,为容器和应用程序提供300秒(5分钟)的启动时间:

1
bash复制代码HEALTHCHECK --start-period=300s java -cp /apps/myapp/healthcheck.jar com.jenkov.myapp.HealthCheck https://localhost/healthcheck

健康检查超时时间

健康检查很有可能超时,如果HEALTCHECK命令需要超过给定时间限制才完成,Docker将认为健康检查超时。可以使用--timeout参数设置超时时间,如下是设置超时时间为5秒的示例:

1
bash复制代码HEALTHCHECK --timeout=5s java -cp /apps/myapp/healthcheck.jar com.jenkov.myapp.HealthCheck https://localhost/healthcheck

注意,如果健康检查超时,Docker也会认为容器不健康。

健康检查重复次数

如果HEALTHCHECK命令执行失败,有可能是结果返回1,或者执行超时,Docker会在认定容器不健康前,重试3次HEALTHCHECK命令,用于检查Docker容器是否返回健康状态。可以通过--retries设置重试次数。下面是将重试次数设置为5的示例:

1
bash复制代码HEALTHCHECK --retries=5 java -cp /apps/myapp/healthcheck.jar com.jenkov.myapp.HealthCheck https://localhost/healthcheck

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

软件研发过程中的5种最常见的图

发表于 2021-11-08

一、背景

软件研发过程中,我们常有如下的困惑:

  1. 有时我们需要设计一个较大型的业务系统,或者做一个开源项目,我们该如何将这个系统的整体功能、逻辑细节一层层描述清楚呢?
  2. 我们接手了一个大型复杂的系统,该如何一点点从宏观到微观的去梳理整个功能流转的脉络呢?

通过简单绘制系统的架构图、各模块之间的接口交互和时序图等,我可以更加直观地理解整个系统的运作模式,所谓的磨刀不误砍柴工。

二、系统架构图

系统架构图往往用于软件研发的总体设计阶段,通过简单分层来展示不同层次的模块,再加上基础服务、公共服务和监控服务等,就构成了系统层面的一个宏观的轮廓。无论是常见的MVC架构、还是DDD架构在整体系统设计层面都是差不多的,一个完整清晰的系统架构图往往会有以下用途:

  • 阐明了系统的各种依赖,包括底层中间件、外部系统、监控系统等,帮助我们更好的建立整个系统的监控体系,了解系统性能瓶颈点等
  • 在业务层阐述了系统的主要功能模块,可以好且快对外介绍我们的系统
  • 阐述了系统的整体技术架构,是微服务化的,还是单体的;有没有网关层、基础组件层等

这里有两张抽象的系统架构供参考:

image1.png

image2.png

三、时序图

时序图一般用于软件研发的详细设计阶段,可以用来描述系统间、微服务间、或者是功能模块间的交互过程,它展示了系统的总体调用链路,和数据流转的过程。基于时序图我们可以做以下事情:

  • 签署服务间的SLA,帮助我们推动微服务治理
  • 宏观上清晰的描述了功能实现的过程(业务流转、数据流转),协助我们在设计时思考,以防遗漏设计细节

3.png

四、程序流程图

详细设计阶段,在系统内部,我们需要清晰的描述业务实现的过程,包括顺序逻辑、条件判断、循环逻辑等。是我们在技术review阶段的重要工具,基本程序流程图设计好,代码中的可能异常和风险点也就分析的差不多了,基本就可以直接照着流程图进行编码了。对于一些比较注重系统稳定性的团队,在此阶段花费的时间,有时要比编码时间还长😂

image3.png

五、状态流转图

有时除了关系业务处理逻辑,还要关心对象状态的流转,这里截取了一个电商网站在下单时的订单状态流转的示例。

43.png

六、总结

本文列举了软件工程设计阶段最为常见的5种图,清晰的软件工程的图可以更加直观的表达出我们的设计意愿,建立起与其他项目参与者沟通的桥梁;还有助于让我们的设计思考更加严密;另外还有助于整体项目文档的建设,帮忙新人快速上手项目。 关于软件工程中常见的5中图就介绍到这里啦,我们下期见,Peace 😘

我是简凡,一个励志用最简单的语言,描述最复杂问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么疑惑,欢迎在我的微信公众号中留言,我还可以为你提供以下帮助:

  • 帮助建立自己的知识体系
  • 互联网真实高并发场景实战讲解
  • 不定期分享Golang、Java相关业内的经典场景实践

我的博客:besthpt.github.io/

微信公众号:”简凡丶”

本文转载自: 掘金

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

Java 并发基础(四):再谈 CyclicBarrier

发表于 2021-11-08

「这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战」

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

java.util.concurrent.CyclicBarrier 也是 JDK 1.5 提供的一个同步辅助类(为什么用也呢?参见 再谈 CountDownLatch),它允许一组线程互相等待,直到到达某个临界点(a common barrier point,翻译成公共障碍点、公共栅栏点都不够传神,直接用临界点吧)。在某个程序中,一组固定大小的线程必须互相等待时,CyclicBarrier 将起很大的作用。因为在等待线程被释放后,这个临界点可以重用,所以说是循环的。

CyclicBarrier 支持一个可选的 Runnable,在一组线程中的最后一个线程完成之后、释放所有线程之前,该 Runnable 在屏障点运行一次(每循环一次 Runnable 运行一次)。这种方式可以用来在下一波继续运行的线程运行之前更新共享状态(比如下一波僵尸来之前,检查武器弹药)。

CountDownLatch 与 CyclicBarrier

CountDownLatch 是不能够重复使用的,是一次性的,其锁定一经打开,就不能够在重复使用。就像引线,点燃后就在燃烧减少,燃烧完了就不能再次使用了。CyclicBarrier 是一种循环的方式进行锁定,这次锁定被打开之后,还能够重复计数,再次使用。就像沙漏,这次漏完了,倒过来接着漏。

还有一点是两者之间很大的区别,就是 CountDownLatch 在等待子线程的过程中,会锁定主线程,而 CyclicBarrier 不会锁定主线程,只是在所有子线程结束后,根据定义执行其可选的 Runnable 线程。

所以在这两种辅助类中进行选择时,能够很明显进行区分。

CyclicBarrier 实例

可以考虑这么一种情况,我们需要向数据库导入一些数据,没导入几条希望能进行一次计时,便于我们查看。因为实现比较简单,直接上代码:

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复制代码import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierTest {
    public static void main(String[] args) throws InterruptedException {
        final long start = System.currentTimeMillis();
        final CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                long end = System.currentTimeMillis();
                System.out.println("导入" + 3 + "条数据,至此总共用时:" + (end - start)
                        + "毫秒");
            }
        });

        for (int i = 0; i < 9; i++) {
            final int threadID = i + 1;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(new Random().nextInt(10));// 模拟业务操作
                        System.out.println(threadID + "完成导入操作。");
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
        System.out.println("====主线程结束====");
    }
}

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
diff复制代码====主线程结束====
4 完成导入操作。
2 完成导入操作。
1 完成导入操作。
导入 3 条数据,至此总共用时:4006 毫秒
5 完成导入操作。
6 完成导入操作。
8 完成导入操作。
导入 3 条数据,至此总共用时:4007 毫秒
3 完成导入操作。
0 完成导入操作。
7 完成导入操作。
导入 3 条数据,至此总共用时:8006 毫秒

程序没导入 3 条会进行一次计时,统计已经执行的时间。如果 CyclicBarrier 构造函数的数字和 for 循环的次数相等的话,这个就是总共用时。

扩展

考虑一下上面的例子,如果 for 循环的次数不是 CyclicBarrier 监听次数的整数倍,比如是 10,那执行结果将会是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff复制代码====主线程结束====
2 完成导入操作。
5 完成导入操作。
4 完成导入操作。
导入 3 条数据,至此总共用时:4005 毫秒
8 完成导入操作。
1 完成导入操作。
3 完成导入操作。
导入 3 条数据,至此总共用时:5005 毫秒
7 完成导入操作。
6 完成导入操作。
0 完成导入操作。
导入 3 条数据,至此总共用时:8005 毫秒
9 完成导入操作。

在打印完“9 完成导入操作。”之后,将一直等待。在这里可以通过 barrier.getNumberWaiting() 查看还差多少个线程达到屏障点。如果出现这种情况,那就需要和 CountDownLatch 配合使用了,当子线程全部执行完,有判断 barrier.getNumberWaiting() 不等于 0,则调用 barrier.reset() 重置。这个时候将会触发 BrokenBarrierException 异常,但是将结束整个过程。

修改的代码如下:

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
java复制代码import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

public class CyclicBarrierTest {
    public static void main(String[] args) throws InterruptedException {
        final long start = System.currentTimeMillis();
        final CountDownLatch count = new CountDownLatch(10);
        final CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
            @Override
            public void run() {
                long end = System.currentTimeMillis();
                System.out.println("导入" + 3 + "条数据,至此总共用时:" + (end - start)
                        + "毫秒");
            }
        });

        for (int i = 0; i < 10; i++) {
            final int threadID = i + 1;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        TimeUnit.SECONDS.sleep(new Random().nextInt(10));// 模拟业务操作
                        System.out.println(threadID + "完成导入操作。");
                        count.countDown();
                        barrier.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        System.out.println("触发 BrokenBarrierException 异常。");
                    }
                }
            }).start();
        }
        count.await();
        
        if(barrier.getNumberWaiting() != 0) {
            System.out.println("不是整数倍。都已执行完,重置 CyclicBarrier。");
            barrier.reset();
        }

        System.out.println("====主线程结束====");
    }
}

执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
diff复制代码3 完成导入操作。
9 完成导入操作。
6 完成导入操作。
导入 3 条数据,至此总共用时:3005 毫秒
8 完成导入操作。
5 完成导入操作。
10 完成导入操作。
导入 3 条数据,至此总共用时:7005 毫秒
1 完成导入操作。
7 完成导入操作。
4 完成导入操作。
2 完成导入操作。
导入 3 条数据,至此总共用时:9005 毫秒
不是整数倍。都已执行完,重置 CyclicBarrier。
====主线程结束====
触发 BrokenBarrierException 异常。

使用 barrier.reset() 进行重置,因为 CyclicBarrier 是一个循环,开头就是结尾,所以重置也可以理解为直接完成。

另外,因为使用了 CountDownLatch,所以主线程会锁定,直到线程通过 count.await() 向下执行。

推荐阅读

  • Java 并发基础(一):synchronized 锁同步
  • Java 并发基础(二):主线程等待子线程结束
  • Java 并发基础(三):再谈 CountDownLatch
  • Java 并发基础(四):再谈 CyclicBarrier
  • Java 并发基础(五):面试实战之多线程顺序打印

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

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

1…392393394…956

开发者博客

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