源码分析之Eureka客户端源码解析

源码分析之Eureka客户端源码解析

说明

本文所分析代码版本为spring boot/cloud 2.2.6。

预备知识

一些注解的说明

eureka中用到几个比较有意思的注解,简化程序实现。

@ConfigurationProperties(“eureka.instance”)

表示从外部配置文件中(properties或是yml文件)读取”eureka.instance”对应的配置。

@ConditionalOnBean/@ConditionalOnClass
1
2
3
4
@ConditionalOnBean         //   当给定的在bean存在时,则实例化当前Bean
@ConditionalOnMissingBean // 当给定的在bean不存在时,则实例化当前Bean
@ConditionalOnClass       // 当给定的类名在类路径上存在,则实例化当前Bean
@ConditionalOnMissingClass // 当给定的类名在类路径上不存在,则实例化当前Bean

可以参考这篇文章:SpringBoot(16)—@ConditionalOnBean与@ConditionalOnClass

比如EurekaClientAutoConfiguration类定义中,类上面注解了@ConditionalOnClass(EurekaClientConfig.class),表示当在类路径中存在EurekaClientConfig.class,则实例化当前EurekaClientAutoConfiguration

@ImplementedBy

google guice注解,指定接口默认的实现类。

@Singleton

jdk 提供的注解,将当前类实现为单例模式。

eureka架构

eureka客户端源码

eureka客户端工作流程

\

下面将简要说明上图的过程。

eureka客户端比较关键的类如下:

其中,DiscoveryClient是核心,它实现了EurekaClient接口,是该接口的默认实现(@ImplementedBy传入的是DiscoveryClient.class)。另外,@Singleton注解声明了DiscoveryClient是单例。

DiscoverClient

该类的关键方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//服务注册相关
boolean register()
void unregister()

//服务续约
boolean renew()

//关闭eureka client
public synchronized void shutdown()

//检查当前client状态,当client状态发生变化时,将会触发新的注册事件,去更新eureka server的注册表中的服务实例信息。
public void registerHealthCheck(HealthCheckHandler healthCheckHandler)

//注册事件监听器,当实例信息有变时,触发对应的处理事件。
public void registerEventListener(EurekaEventListener eventListener)

其构造方法DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer)中,此方法中依次执行了 从eureka server中拉取注册表,服务注册,初始化发送心跳,缓存刷新(定时拉取注册表信息),按需注册定时任务等,贯穿了Eureka 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
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
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args, Provider<BackupRegistry> backupRegistryProvider, EndpointRandomizer endpointRandomizer) {
    ......
     //shouldFetchRegistry,其实现类为EurekaClientConfigBean,找到它其实对应于:eureka.client.fetch-register,true:表示client从server拉取注册表信息。
    if (config.shouldFetchRegistry()) {
        this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
    } else {
        this.registryStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
    }
   
   //shouldRegisterWithEureka,点其实现类EurekaClientConfigBean,找到它其实对应于:eureka.client.register-with-eureka:true:表示client将注册到server。
   if (config.shouldRegisterWithEureka()) {
       this.heartbeatStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRATION_PREFIX + "lastHeartbeatSec_", new long[]{15L, 30L, 60L, 120L, 240L, 480L});
  } else {
       this.heartbeatStalenessMonitor = ThresholdLevelsMetric.NO_OP_METRIC;
  }
  ......
       
// default size of 2 - 1 each for heartbeat and cacheRefresh
   scheduler = Executors.newScheduledThreadPool(2,
                                                    new ThreadFactoryBuilder()
                                                    .setNameFormat("DiscoveryClient-%d")
                                                    .setDaemon(true)
                                                    .build());
   //用于发送心跳
   heartbeatExecutor = new ThreadPoolExecutor(
       1, clientConfig.getHeartbeatExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
       new SynchronousQueue<Runnable>(),
       new ThreadFactoryBuilder()
      .setNameFormat("DiscoveryClient-HeartbeatExecutor-%d")
      .setDaemon(true)
      .build()
  );  // use direct handoff
   //用于刷新缓存
   cacheRefreshExecutor = new ThreadPoolExecutor(
       1, clientConfig.getCacheRefreshExecutorThreadPoolSize(), 0, TimeUnit.SECONDS,
       new SynchronousQueue<Runnable>(),
       new ThreadFactoryBuilder()
      .setNameFormat("DiscoveryClient-CacheRefreshExecutor-%d")
      .setDaemon(true)
      .build()
  );  // use direct handoff
   
   //eurekaTransport是eureka Client和eureka server进行http交互jersey客户端。点开EurekaTransport,看到许多httpclient相关的属性。
   eurekaTransport = new EurekaTransport();
   
   if (clientConfig.shouldFetchRegistry()) {
....
           //从eureka server拉取注册表中的信息,将注册表缓存到本地,可以就近获取其他服务信息,减少于server的交互。
        boolean primaryFetchRegistryResult = fetchRegistry(false);
        ......
  }
   
   //register()进行注册,若注册失败则抛出异常
   if (clientConfig.shouldRegisterWithEureka() && clientConfig.shouldEnforceRegistrationAtInit()) {
  try {
           if (!register() ) {
          throw new IllegalStateException("Registration error at startup. Invalid server response.");
          }
  } catch (Throwable th) {
           logger.error("Registration error at startup: {}", th.getMessage());
           throw new IllegalStateException(th);
  }
  }
   
   //启动调度任务
   initScheduledTasks();
}

DiscoveryClient构造方法小结

构造方法中主要进行如下工作:

  • 初始化各种信息
  • 从eureka server拉取注册表信息
  • 向server注册自己
  • 初始化3个任务

以下对重要过程进行分析:

拉取注册表过程

对应private boolean fetchRegistry(boolean forceFullRegistryFetch)方法,参考注释、源码可以知道逻辑如下:

1
2
3
4
5
6
7
如果增量式拉取被禁止或第一次拉取注册表,则进行全量拉取:getAndStoreFullRegistry()。
否则进行增量拉取注册表信息getAndUpdateDelta(applications)。
一般情况,在Eureka client第一次启动,会进行全量拉取。之后的拉取都尽量尝试只进行增量拉取。

拉取服务注册表:
全量拉取:getAndStoreFullRegistry();
增量拉取:getAndUpdateDelta(applications);
全量拉取 getAndStoreFullRegistry()
1
2
3
4
5
6
7
进入getAndStoreFullRegistry() 方法,有一方法:eurekaTransport.queryClient.getApplications。
通过debug发现 实现类是AbstractJerseyEurekaHttpClient,点开,debug出
webResource地址为:http://root:root@eureka-7900:7900/eureka/apps/,此端点用于获取server中所有的注册表信息。
getAndStoreFullRegistry()可能被多个线程同时调用,导致新拉取的注册表被旧的覆盖(如果新拉取的动作设置apps阻塞的情况下)。
此时用了AutomicLong来进行版本管理,如果更新时版本不一致,不保存apps。
通过这个判断fetchRegistryGeneration.compareAndSet(currentUpdateGeneration, currentUpdateGeneration + 1),如果版本一致,并设置新版本(+1),
接着执行localRegionApps.set(this.filterAndShuffle(apps));过滤并洗牌apps。点开this.filterAndShuffle(apps)实现,继续点apps.shuffleAndIndexInstances,继续点shuffleInstances,继续点application.shuffleAndStoreInstances,继续点_shuffleAndStoreInstances,发现if (filterUpInstances && InstanceStatus.UP != instanceInfo.getStatus())。只保留状态为UP的服务。
增量拉取getAndUpdateDelta(applications)
1
2
3
4
5
6
7
8
9
10
11
回到刚才的fetchRegistry方法中,getAndUpdateDelta,增量拉取。通过getDelta方法,看到实际拉取的地址是:apps/delta,如果获取到的delta为空,则全量拉取。
通常来讲是3分钟之内注册表的信息变化(在server端判断),获取到delta后,会更新本地注册表。
增量式拉取是为了维护client和server端 注册表的一致性,防止本地数据过久,而失效,采用增量式拉取的方式,减少了client和server的通信量。
client有一个注册表缓存刷新定时器,专门负责维护两者之间的信息同步,但是当增量出现意外时,定时器将执行,全量拉取以更新本地缓存信息。更新本地注册表方法updateDelta,有一个细节。
if (ActionType.ADDED.equals(instance.getActionType())) ,public enum ActionType {
        ADDED, // Added in the discovery server
        MODIFIED, // Changed in the discovery server
        DELETED
        // Deleted from the discovery server
    },
在InstanceInfo instance中有一个instance.getActionType(),ADDED和MODIFIED状态的将更新本地注册表applications.addApplication,DELETED将从本地剔除掉existingApp.removeInstance(instance)。
服务注册过程
1
2
DiscoveryClient构造函数中,拉取fetchRegistry完后进行register注册。由于构造函数开始时已经将服务实例元数据封装好了instanceInfo,所以此处之间向server发送instanceInfo,
通过方法httpResponse = eurekaTransport.registrationClient.register(instanceInfo);看到String urlPath = "apps/" + info.getAppName();又是一个server端点,退上去f7,httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();204状态码,则注册成功。
初始化3个定时任务

有3个:

  • 心跳定时任务:client会定时向server发送心跳,维持自己服务租约的有效性,用心跳定时任务实现;
  • 缓存定时任务:server中会有不同的服务实例注册或是下线,所以client需要定时从server拉取注册表信息,用缓存定时任务实现。
  • 按需注册的定时任务,当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。

参见initScheduledTasks()。

心跳定时任务和缓存刷新定时任务是有scheduler 的 schedule提交的,查看scheduler变量声明上,看到注释:

1
2
3
A scheduler to be used for the following 3 tasks:
- updating service urls
- scheduling a TimedSuperVisorTask。

由此可以知道循环逻辑是由TimedSuperVisorTask实现的。

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
//心跳定时任务
 new TimedSupervisorTask(
                             "heartbeat",
                             scheduler,
                             heartbeatExecutor,
                             renewalIntervalInSecs,
                             TimeUnit.SECONDS,
                             expBackOffBound,
                             new HeartbeatThread())
 //HeartbeatThread线程内部run方法定义如下:
class CacheRefreshThread implements Runnable{
   public void run() {
       if (renew()) {
           lastSuccessfulHeartbeatTimestamp = System.currentTimeMillis();
      }
}
}

 
//缓存定时任务
 scheduler.schedule(
                     new TimedSupervisorTask(
                             "cacheRefresh",
                             scheduler,
                             cacheRefreshExecutor,
                             registryFetchIntervalSeconds,
                             TimeUnit.SECONDS,
                             expBackOffBound,
                             new CacheRefreshThread()
                    ),
//CacheRefreshThread线程内部run方法定义如下:
class CacheRefreshThread implements Runnable {
         public void run() {
             refreshRegistry();
        }
    }

//还有一个定时任务,按需注册。当instanceinfo和status发生变化时,需要向server同步,去更新自己在server中的实例信息。保证server注册表中服务实例信息的有效和可用。
 // InstanceInfo replicator
         instanceInfoReplicator = new InstanceInfoReplicator(
                 this,
                 instanceInfo,
                 clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                 2); // burstSize
 
         statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
             @Override
             public String getId() {
                 return "statusChangeListener";
            }
 
          @Override
             public void notify(StatusChangeEvent statusChangeEvent) {
                 if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                         InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                     // log at warn level if DOWN was involved
                     logger.warn("Saw local status change event {}", statusChangeEvent);
                } else {
                     logger.info("Saw local status change event {}", statusChangeEvent);
                }
                 instanceInfoReplicator.onDemandUpdate();
            }
        };
         if (clientConfig.shouldOnDemandUpdateStatusChange()) {
             applicationInfoManager.registerStatusChangeListener(statusChangeListener);
        }
     instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());    
     

//此定时任务有2个部分,
// 1:定时刷新服务实例信息和检查应用状态的变化,在服务实例信息发生改变的情况下向server重新发起注册。InstanceInfoReplicator点进去。看到一个方法    
 
 public void run() {
         try {
             discoveryClient.refreshInstanceInfo();//刷新instanceinfo。
//如果实例信息有变,返回数据更新时间。
             Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
             if (dirtyTimestamp != null) {
                 discoveryClient.register();//注册服务实例。
                 instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
             logger.warn("There was a problem with the instance info replicator", t);
        } finally {
         //延时执行下一个检查任务。用于再次调用run方法,继续检查服务实例信息和状态的变化。
             Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
             scheduledPeriodicRef.set(next);
        }
    }      

refreshInstanceInfo点进去,看方法注释:如果有变化,在下次心跳时,同步向server。

//2.注册状态改变监听器,在应用状态发生变化时,刷新服务实例信息,在服务实例信息发生改变时向server注册。 看这段            
  statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                 @Override
                 public String getId() {
                     return "statusChangeListener";
                }
@Override
             public void notify(StatusChangeEvent statusChangeEvent) {
                 if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                         InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                     // log at warn level if DOWN was involved
                     logger.warn("Saw local status change event {}", statusChangeEvent);
                } else {
                     logger.info("Saw local status change event {}", statusChangeEvent);
                }
                 instanceInfoReplicator.onDemandUpdate();
            }
        };如果状态发生改变,调用onDemandUpdate(),点onDemandUpdate进去,看到InstanceInfoReplicator.this.run();    
         
//总结:两部分,一部分自己去检查,一部分等待状态监听事件。

//初始化定时任务完成,最后一步启动步骤完成。接下来就是正常服务于业务。然后消亡。          
服务下线

服务下线:在应用关闭时,client会向server注销自己,在Discoveryclient销毁前,会执行下面清理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//此方法上有一个注解,表示:在销毁前执行此方法。unregisterStatusChangeListener注销监听器。
@PreDestroy
@Override
public synchronized void shutdown() {
  ......
       //取消定时任务。
  cancelScheduledTasks();
  ......
   //服务下线。
   unregister();
   //关闭jersy客户端  
   eurekaTransport.shutdown();
   //其他操作
  ......
}

unregister点进去。cancel点进去。AbstractJerseyEurekaHttpClient。String urlPath = "apps/" + appName + '/' + id;看到url和http请求delete方法。      

更多

更进一步的启动流程分析,可以参考这篇文章:Eureka之Client端注册

下面这张图即来自该文章,感觉还可以。

在一个spring cloud项目中,eureka客户端中的关键类DiscoveryClient是怎样实例化的?

首先,需要了解spring boot如何使用spring factories机制加载第三方库,这个可以参考我之前整理的这篇文章:源码分析之Spring Boot如何利用Spring Factories机制进行自动注入

以笔者本人使用eureka时所使用的spring-cloud-netflix-eureka-client-2.2.2.RELEASE为例,在pom文件中引入该包后,可以IDEA中看到该包下包含了spring.factories

该文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaDiscoveryClientConfigServiceBootstrapConfiguration

我们点EurekaDiscoveryClientConfigServiceBootstrapConfiguration,可以看到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

@ConditionalOnClass(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.discovery.enabled",
matchIfMissing = false)
@Configuration(proxyBeanMethods = false)
@Import({ EurekaDiscoveryClientConfiguration.class, // this emulates
// @EnableDiscoveryClient, the import
// selector doesn't run before the
// bootstrap phase
EurekaClientAutoConfiguration.class,
EurekaReactiveDiscoveryClientConfiguration.class,
ReactiveCommonsClientAutoConfiguration.class })
public class EurekaDiscoveryClientConfigServiceBootstrapConfiguration {
}

在EurekaClientAutoConfiguration中,可以找到如下代码:

1
2
CloudEurekaClient cloudEurekaClient = new CloudEurekaClient(appManager,
config, this.optionalArgs, this.context);

CloudEurekaClient类,实际上继承自DiscoveryClient

1
2
3
public class CloudEurekaClient extends DiscoveryClient {
  ......
}

所以,在spring boot启动时,将通过spring factories机制,实例化DiscoveryClient。

源码分析之Java线程池ThreadPoolExecutor

源码分析之Java线程池ThreadPoolExecutor

我个人觉得,学习源码的姿势,首先是需要知道想学习的框架/工具如何使用,然后接下来再去看源码注释,看看当时代码作者是如何阐述代码的,再去看代码怎么编写,效果才最佳。

同样的,接下来要分析的线程池,首先用途自不必说,不管有没有用过,ThreadPoolExecutor的运行机制、传说中的7个参数(核心线程数corePoolSize、最大线程数maxPoolSize、等待时间keepAliveTime、时间单位timeUnit、阻塞队列blockingQueue、线程工厂threadFactory、拒绝策略rejectHandler),相信大家都已经熟练掌握,此处不再赘述。

接下来简单过一下ThreadPoolExecutor的注释。\

0. 源码注释中的关键点

ThreadPoolExecutor的类注释中,主要阐述如下内容:

  • 有关运行机制、7个参数如何使用与配合
    • 还有一些细节,比如阻塞队列,可以有3种选择:
    • direct handoff
    • unbounded queue
    • bounded queue
    • 其他可能有用的信息:
    • 线程池的hook方法:beforeExecute(Thread, Runnable), afterExecute(Runnable, Throwable)。若hook抛出异常,则当前线程也会异常终止。
    • getQueue() 可以获取工作队列,可以用来monitor和debug,不推荐用于其他用途。提供了两个接口: remove(Runnable) , purge()
    • 线程池对象在程序中不被引用,并且没有线程时,该线程池将会被自动关闭。
  • 线程池状态的说明
    • ThreadPoolExecutor有一个AtomicInteger变量ctl表示,该变量包含了两个含义:

    • 高3位表示当前线程池状态

      • “`\
      • \
      • RUNNING: Accept new tasks and process queued tasks
      • \
      • SHUTDOWN: Don't accept new tasks, but process queued tasks
      • \
      • STOP: Don't accept new tasks, don't process queued tasks,
      • \
      • and interrupt in-progress tasks
      • \
      • TIDYING: All tasks have terminated, workerCount is zero,
      • \
      • the thread transitioning to state TIDYING
      • \
      • will run the terminated() hook method
      • \
      • TERMINATED: terminated() has completed
      • \
      - 状态转移。这些状态支持比较,因为JDK实现时,The runState monotonically increases over time, but need not hit each state.
      
      
        \
      • RUNNING -> SHUTDOWN
      • \
      • On invocation of shutdown(), perhaps implicitly in finalize()
      • \
      • (RUNNING or SHUTDOWN) -> STOP
      • \
      • On invocation of shutdownNow()
      • \
      • SHUTDOWN -> TIDYING
      • \
      • When both queue and pool are empty
      • \
      • STOP -> TIDYING
      • \
      • When pool is empty
      • \
      • TIDYING -> TERMINATED
      • \
      • When the terminated() hook method has completed “`
    • 剩余的低29位表示线程池的数量(所以目前的版本只有支持2^29-1个线程)

下面将结合代码,解说线程池的关键流程。

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
29
30
31
32
33
34
35
// 1. `ctl`,可以看做一个int类型的数字,高3位表示线程池状态,低29位表示worker数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 2. `COUNT_BITS`,`Integer.SIZE`为32,所以`COUNT_BITS`为29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 3. `CAPACITY`,线程池允许的最大线程数。1左移29位,然后减1,即为 2^29 - 1,二进制表示为连续的29个1
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
// 4. 线程池有5种状态,按大小排序如下:RUNNING < SHUTDOWN < STOP < TIDYING < TERMINATED . 此处RUNNING实际上是-(1<<COUNT_BITS),是负数。
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

// Packing and unpacking ctl
// 5. `runStateOf()`,获取线程池状态,通过按位与操作,低29位将全部变成0
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 6. `workerCountOf()`,获取线程池worker数量,通过按位与操作,高3位将全部变成0
private static int workerCountOf(int c) { return c & CAPACITY; }
// 7. `ctlOf()`,根据线程池状态和线程池worker数量,生成ctl值
private static int ctlOf(int rs, int wc) { return rs | wc; }

/*
* Bit field accessors that don't require unpacking ctl.
* These depend on the bit layout and on workerCount being never negative.
*/
// 8. `runStateLessThan()`,线程池状态小于xx
private static boolean runStateLessThan(int c, int s) {
return c < s;
}
// 9. `runStateAtLeast()`,线程池状态大于等于xx
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}

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
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
// 基本类型参数校验
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
// 空指针校验
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
// 根据传入参数`unit`和`keepAliveTime`,将存活时间转换为纳秒存到变量`keepAliveTime `中
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}

3. 提交执行task的过程

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
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
// worker数量比核心线程数小,直接创建worker执行任务
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// worker数量超过核心线程数,任务直接进入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 线程池状态不是RUNNING状态,说明执行过shutdown命令,需要对新加入的任务执行reject()操作。
// 这儿为什么需要recheck,是因为任务入队列前后,线程池的状态可能会发生变化。
if (! isRunning(recheck) && remove(command))
reject(command);
// 这儿为什么需要判断0值,主要是在线程池构造方法中,核心线程数允许为0
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果线程池不是运行状态,或者任务进入队列失败,则尝试创建worker执行任务。
// 这儿有3点需要注意:
// 1. 线程池不是运行状态时,addWorker内部会判断线程池状态
// 2. addWorker第2个参数表示是否创建核心线程
// 3. addWorker返回false,则说明任务执行失败,需要执行reject操作
else if (!addWorker(command, false))
reject(command);
}

4. addworker源码解析

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
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
//外层自旋
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// 这个条件写得比较难懂,和下面的条件等价
// (rs > SHUTDOWN) ||
// (rs == SHUTDOWN && firstTask != null) ||
// (rs == SHUTDOWN && workQueue.isEmpty())
// 1. 线程池状态大于SHUTDOWN时,直接返回false
// 2. 线程池状态等于SHUTDOWN,且firstTask不为null,直接返回false
// 3. 线程池状态等于SHUTDOWN,且队列为空,直接返回false
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

// 内层自旋
for (;;) {
int wc = workerCountOf(c);
// worker数量超过容量,直接返回false
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 使用CAS的方式增加worker数量。
// 若增加成功,则直接跳出外层循环进入到第二部分
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 线程池状态发生变化,对外层循环进行自旋
if (runStateOf(c) != rs)
continue retry;
// 其他情况,直接内层循环进行自旋即可
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
// worker的添加必须是串行的,因此需要加锁
mainLock.lock();
try {
// 这儿需要重新检查线程池状态
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// worker已经调用过了start()方法,则不再创建worker
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// worker创建并添加到workers成功
workers.add(w);
// 更新`largestPoolSize`变量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 启动worker线程
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// worker线程启动失败,说明线程池状态发生了变化(关闭操作被执行),需要进行shutdown相关操作
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

5. 线程池worker任务单元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;

/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;

/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 这儿是Worker的关键所在,使用了线程工厂创建了一个线程。传入的参数为当前worker
this.thread = getThreadFactory().newThread(this);
}

/** Delegates main run loop to outer runWorker */
public void run() {
//此处分析参见 `核心线程执行逻辑-runworker`
runWorker(this);
}

// 省略代码...
}

6. 核心线程执行逻辑-runworker

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
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
// 调用unlock()是为了让外部可以中断
w.unlock(); // allow interrupts
// 这个变量用于判断是否进入过自旋(while循环)
boolean completedAbruptly = true;
try {
// 这儿是自旋
// 1. 如果firstTask不为null,则执行firstTask;
// 2. 如果firstTask为null,则调用getTask()从队列获取任务。
// 3. 阻塞队列的特性就是:当队列为空时,当前线程会被阻塞等待
while (task != null || (task = getTask()) != null) {
// 这儿对worker进行加锁,是为了达到下面的目的
// 1. 降低锁范围,提升性能
// 2. 保证每个worker执行的任务是串行的
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
// 如果线程池正在停止,则对当前线程进行中断操作
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
// 执行任务,且在执行前后通过`beforeExecute()`和`afterExecute()`来扩展其功能。
// 这两个方法在当前类里面为空实现。
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
// 帮助gc
task = null;
// 已完成任务数加一
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 自旋操作被退出,说明线程池正在结束
processWorkerExit(w, completedAbruptly);
}
}

参考资料

源码分析之Spring Boot如何利用Spring Factories机制进行自动注入

源码分析之Spring Boot如何利用Spring Factories机制进行自动注入

前言

本文所涉及spring/spring boot代码,请参考spring boot 2.2.6对应版本。

我们在刚学习spring boot时,有没有一个困惑:spring boot能够自动实例化很多第三方的依赖库?比如eureka、druid等。这个就涉及到spring boot的扩展机制spring factories。

简单来将,spring factories类似与Java SPI机制,利用该机制,我们能够自定义实现一些SDK或是spring boot starter,其实例化过程由我们来实现,使用方只需要在项目中引入包、不需要或是只需做很少的配置。

Spring Factories的核心

spring factories机制核心在spring-core包中定义的SpringFactoriesLoader类,该类的公有方法只有2个:\

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
根据接口类获取其实现类的实例,这个方法返回的是对象列表。
Load and instantiate the factory implementations of the given type from "META-INF/spring.factories", using the given class loader.
The returned factories are sorted through AnnotationAwareOrderComparator.
If a custom instantiation strategy is required, use loadFactoryNames(java.lang.Class<?>, java.lang.ClassLoader) to obtain all registered factory names.
*/
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader)

/*
根据接口获取其接口类的名称,这个方法返回的是类名的列表。
Load the fully qualified class names of factory implementations of the given type from "META-INF/spring.factories", using the given class loader.
*/
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)

而上面这两个方法,最终都会调用一个SpringFactoriesLoader的私有方法loadSpringFactories,从指定的ClassLoader中获取spring.factories文件,并解析得到类名列表。具体代码如下:

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
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}

try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}

**该代码作用:遍历整个ClassLoader中所有jar包下的spring.factories文件。**spring.factories文件的位置:jar包下META-INF/spring.factories。

有没有感觉很熟悉?Java SPI的读取目录在META-INF/services下,其实大家写代码都是相互参考、然后形成一个约定俗成习惯的。

我们可以在自己的jar中配置spring.factories文件,不会影响到其它地方的配置,也不会被别人的配置覆盖。

示例

举个例子,spring boot start的实现中,如下所示:

spring-boot的spring.factories具体内容如下:

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
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

spring boot如何利用spring.factories进行注入

一个spring boot项目,在启动类上会有@SpringBootApplication注解,该注解实现:

1
2
3
4
5
6
7
8
9
10
11
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}

其中,@EnableAutoConfiguration注解定义大体如下:

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}

其中@Import(AutoConfigurationImportSelector.class) 很关键,@Import注解通过快速导入的方式实现把实例加入spring的IOC容器中,可以用于导入第三方包。AutoConfigurationImportSelector实现了ImportSelector接口,任何实现ImportSelector的类,都会在启动时被spring-context包ConfigurationClassParser中的processImports进行实例化,并执行selectImports方法。

AutoConfigurationImportSelector的selectImports以及相关的方法实现如下:

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
/*
这部分是AutoConfigurationImportSelector的代码
*/
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}

/*
下面是AutoConfigurationMetadataLoader中的代码
*/
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}

static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
: ClassLoader.getSystemResources(path);
Properties properties = new Properties();
while (urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
return loadMetadata(properties);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
}
}

可以看到,AutoConfigurationImportSelector的selectImports方法主要是用于加载类,但为了获取哪些类需要加载,则是通过SpringFactoriesLoader去加载对应的spring.factories。大体调用链路:

1
2
3
4
AutoConfigurationImportSelector#selectImports()
-> AutoConfigurationImportSelector#getAutoConfigurationEntry()
-> AutoConfigurationImportSelector#getCandidateConfigurations()
-> SpringFactoriesLoader#loadFactoryNames()

最终执行的是 loadFactoryNames(EnableAutoConfiguration.class, 当前classloader), 结合上面Spring Factories的核心这一小节,可以获知,SpringFactoriesLoader将会根据EnableAutoConfiguration接口,去所有spring.factoriesEnableAutoConfiguration.class所对应的values,并返回。

常见扩展点

上面已经提到,spring factories需要给出一个spring.factories文件,该文件规定了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
# Auto Configure(这个扩展是使用的最多的,特别是是一些公共SDK,会这借助这扩展实现Bean的自动注入)
org.springframework.boot.autoconfigure.EnableAutoConfiguration

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter

参考资料

源码分析之Kafka Consumer消费消息的过程

源码分析之Kafka Consumer消费消息的过程

说明

本文基于Apache Kafka 2.5.1(2020.09.10拉取最新代码)

Consumer如何使用?

阅读源码前的首先要做到熟悉相关组件的概念、基本使用。而最靠谱的资料就是官方文档。

建议阅读官方文档(https://kafka.apache.org/documentation/)后,自己练习、使用kafka之后再开始阅读源码。

KafkaConsumer的JavaDoc(参见https://kafka.apache.org/10/javadoc/?org/apache/kafka/clients/consumer/KafkaConsumer.html)本身就给出了不少有用信息,下面仅列出一些关键点:

  • Cross-Version Compatibility 客户端支持0.10.0以及以上版本,如果调用不支持的特性,会抛出UnsupportedVersionException

  • Offsets and Consumer Position position: 有待读取的下一条记录的偏移量 commited position: 已被保存、归档的最后一条记录的偏移量,可以用于恢复数据。

  • Consumer Groups and Topic Subscriptions

    • 一个partition内的消息只会投递给group中的一个consumer

    • 以下场景将触发group rebalance

      • 一个consumer挂掉
      • 新加入一个consumer
      • 新的partition加入一个topic
      • 一个新的topic匹配已有的订阅正则(subscribed regx)
    • ConsumerRebalanceListener 可以监听rebalance

  • Detecting Consumer Faillures

    • 调用poll(long)方法时consumer会自动加入到group中,consumer会发心跳给服务器端,超时时间session.timeout.ms

    • consumer可能遇到活锁:锁检测机制 session.timeout.ms

    • poll方法相关配置:

      • max.poll.interval.ms
      • max.poll.records

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//代码1:
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
   ConsumerRecords<String, String> records = consumer.poll(100);
   for (ConsumerRecord<String, String> record : records)
       System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}

主要流程

  1. 设置配置信息,包括broker地址,consumer group id, 自动提交消费的位置,序列化配置
  2. 创建KafkaConsumer对象
  3. 订阅2个topic: foo, bar
  4. 循环拉取并打印

以下将重点解读KafkaConsumer消费流程中的3个问题:

  1. 订阅主题的过程是如何实现的?
  2. consumer如何与coordinator协商,确定消费哪些partition?
  3. 拉取消息的过程是如何实现的?

订阅主题的过程是如何实现的?

仍以上面代码示例为例,跟踪源码中的subscribe方法,最终会看到KafkaConsumer中的如下代码:

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
//代码2:
@Override
public void subscribe(Collection<String> topics, ConsumerRebalanceListener listener) {
   acquireAndEnsureOpen();
   try {
       maybeThrowInvalidGroupIdException();
       if (topics == null)
           throw new IllegalArgumentException("Topic collection to subscribe to cannot be null");
       if (topics.isEmpty()) {
           // treat subscribing to empty topic list as the same as unsubscribing
           this.unsubscribe();
      } else {
           for (String topic : topics) {
               if (topic == null || topic.trim().isEmpty())
                   throw new IllegalArgumentException("Topic collection to subscribe to cannot contain null or empty topic");
          }

           throwIfNoAssignorsConfigured();
           fetcher.clearBufferedDataForUnassignedTopics(topics);
           log.info("Subscribed to topic(s): {}", Utils.join(topics, ", "));
           if (this.subscriptions.subscribe(new HashSet<>(topics), listener))
               metadata.requestUpdateForNewTopics();
      }
  } finally {
       release();
  }
}

基本流程如下:

  1. 加轻量级锁(通过CAS方式加锁)

  2. 参数校验

    1. 注意:如果传入topic集合为空,则直接走unsubscribe的逻辑
  3. 重置订阅状态subscriptions,更新元数据metadata中的topic信息

    1. 订阅状态维护:订阅的 topic 和 patition 的消费位置等状态信息
    2. 元数据metada维护:Kafka 集群元数据的一个子集,包括集群的 Broker 节点、Topic 和 Partition 在节点上分布,Coordinator 给 Consumer 分配的 Partition 信息。

经典思路:主动检测不支持的情况并抛出异常,避免系统产生不可预期的行为

KafkaConsumer的Javadoc明确声明了,consumer不是线程安全的,被并发调用时会出现不可预期的结果。为了避免这种情况发生,Kafka 做了主动的检测并抛出异常,而不是放任系统产生不可预期的情况。

Kafka“主动检测不支持的情况并抛出异常,避免系统产生不可预期的行为”这种模式,对于增强的系统的健壮性是一种非常有效的做法。如果你的系统不支持用户的某种操作,正确的做法是,检测不支持的操作,直接拒绝用户操作,并给出明确的错误提示,而不应该只是在文档中写上“不要这样做”,却放任用户错误的操作,产生一些不可预期的、奇怪的错误结果。

引自:极客时间:消息队列高手课

具体代码就是上面的代码2中的acquireAndEnsureOpen(),具体实现如下:

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
/**
    * Acquire the light lock and ensure that the consumer hasn't been closed.
    * @throws IllegalStateException If the consumer has been closed
    */
private void acquireAndEnsureOpen() {
   acquire();
   //KafkaConsumer成员变量,初始值为false,调用close(Duration)方法后才会置为true
   if (this.closed) {
       release();
       throw new IllegalStateException("This consumer has already been closed.");
  }
}

//变量声明
private static final long NO_CURRENT_THREAD = -1L;

private void acquire() {
   //拿到当前线程的线程id
   long threadId = Thread.currentThread().getId();
   /*if threadId与当前正执行的线程的id不一致(并发,多线程访问)&& threadId对应的线程没有争抢到锁
  then 抛出异常
  举例:
  现在有两个KafkaConsumer线程,线程id分别是thread1, thread2,要执行acquire()方法。
thread1先启动,执行完上面这条语句、赋值threadId后, thread1栈帧中threadId=thread1,此时CPU线程调度、执行thread2,
thread2也走到if语句时,在thread2的栈帧中,threadId已经赋值为thread2,走到这里,currentThread作为成员变量,初始值为NO_CURRENT_THREAD(-1),因此必然不相等,继续走第二判断条件,即利用AtomicInteger的CAS操作,将当前线程id threadId(thread2)赋值给currentThread这个AtomicInteger,必然返回true,因此会继续执行,使得refcount加1;
接着,此时执行thread1,那么再继续执行if,threadId(thread1) != currentThread.get() (thread2)能满足,但是currentThread的CAS赋值将会失败,因此此时currentThread的值并不是NO_CURRENT_THREAD。

refcount用于记录重入锁的情况,参见release()方法,当refcount=0时,currentThread将重新赋值为NO_CURRENT_THREAD,保证彻底解锁。
   */
   if (threadId != currentThread.get() && !currentThread.compareAndSet(NO_CURRENT_THREAD, threadId))
       throw new ConcurrentModificationException("KafkaConsumer is not safe for multi-threaded access");
   refcount.incrementAndGet();
}

/**
    * Release the light lock protecting the consumer from multi-threaded access.
    */
private void release() {
   if (refcount.decrementAndGet() == 0)
       currentThread.set(NO_CURRENT_THREAD);
}

此处通过AtomicLong currentThread, AtomicInteger refcount 这两个变量来实现轻量级锁的机制非常经典,建议学习、理解、运用。具体分析我已经写在了上述源码中供大家参考。

有关元数据更新

元数据更新调用的是metadata.requestUpdateForNewTopics();,里面内容是:

1
2
3
4
5
6
7
public synchronized int requestUpdateForNewTopics() {
   // Override the timestamp of last refresh to let immediate update.
   this.lastRefreshMs = 0;
   this.needPartialUpdate = true;
   this.requestVersion++;
   return this.updateVersion;
}

可以看到,内部并没有更新元数据的操作,只是设置了几个标志位。但我们知道,在消息消息前,我们必须获取元数据,比如broker节点信息、topic和partition的分布,否则根本不知道从哪里获取数据。那么此时,我们就可以根据这几个标志位来去找相应代码。不过从KafkaConsumer的JavaDoc提供的信息也能获知,是在拉取消息时将会根据这些标志位来更新元数据。具体逻辑请看接下来的分析。

拉取消息的过程是如何实现的?

我们已经知道,拉取消息是调用KafkaConsumer的poll(Duration)方法,主要流程的序列图参考下图

注:引自 极客时间 消息队列高手课

poll源码如下,主要流程在:

  1. updateAssignmentMetadataIfNeeded(): 更新元数据
  2. pollForFetches():拉取消息
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
@Override
public ConsumerRecords<K, V> poll(final Duration timeout) {
   return poll(time.timer(timeout), true);
}

/**
    * @throws KafkaException if the rebalance callback throws exception
    */
private ConsumerRecords<K, V> poll(final Timer timer, final boolean includeMetadataInTimeout) {
   //加锁,保证只有1个线程读取数据
   acquireAndEnsureOpen();
   try {
       //记录开始时间
       this.kafkaConsumerMetrics.recordPollStart(timer.currentTimeMs());

       if (this.subscriptions.hasNoSubscriptionOrUserAssignment()) {
           //没有订阅信息,抛异常
           throw new IllegalStateException("Consumer is not subscribed to any topics or assigned any partitions");
      }

       do {
           //这个地方没太看懂
           client.maybeTriggerWakeup();

           if (includeMetadataInTimeout) {
               // try to update assignment metadata BUT do not need to block on the timer for join group
               updateAssignmentMetadataIfNeeded(timer, false);
          } else {
               /*
updateAssignmentMetadataIfNeeded方法有3个作用:
                   - discovery coordinator if necessary
                   - join group if necessary
                   - refresh metadata and fetch position if necessary
*/
               while (!updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE), true)) {
                   log.warn("Still waiting for metadata");
              }
          }

           final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollForFetches(timer);
           if (!records.isEmpty()) {
               // before returning the fetched records, we can send off the next round of fetches
               // and avoid block waiting for their responses to enable pipelining while the user
               // is handling the fetched records.
               //
               // NOTE: since the consumed position has already been updated, we must not allow
               // wakeups or any other errors to be triggered prior to returning the fetched records.
               if (fetcher.sendFetches() > 0 || client.hasPendingRequests()) {
                   client.transmitSends();
              }

               return this.interceptors.onConsume(new ConsumerRecords<>(records));
          }
      } while (timer.notExpired());

       return ConsumerRecords.empty();
  } finally {
       release();
       this.kafkaConsumerMetrics.recordPollEnd(timer.currentTimeMs());
  }
}

boolean updateAssignmentMetadataIfNeeded(final Timer timer, final boolean waitForJoinGroup) {
   if (coordinator != null && !coordinator.poll(timer, waitForJoinGroup)) {
       return false;
  }
/*
Set the fetch position to the committed position (if there is one) or reset it using the offset reset policy the user has configured.
更新position或是根据用户配置重置position
*/
   return updateFetchPositions(timer);
}


updateAssignmentMetadataIfNeeded() 更新元数据

updateAssignmentMetadataIfNeeded方法有3个作用(此处代码最近有更新,可以看看github中代码更新记录):

  • discovery coordinator if necessary
  • join group if necessary
  • refresh metadata and fetch position if necessary

Coordinator#poll() 维持心跳,更新元数据

updateAssignmentMetadataIfNeeded调用了coordinator.poll() 方法,即ConsumerCoordinator#poll(Timer timer, boolean waitForJoinGroup),源码如下

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
public boolean poll(Timer timer, boolean waitForJoinGroup) {
   maybeUpdateSubscriptionMetadata();

   invokeCompletedOffsetCommitCallbacks();
//先忽略细节逻辑,第一次启动时,将执行此处
   if (subscriptions.hasAutoAssignedPartitions()) {
       if (protocol == null) {
           throw new IllegalStateException("User configured " + ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG +
                                           " to empty while trying to subscribe for group protocol to auto assign partitions");
      }
/*
此处是发送心跳逻辑
*/
       pollHeartbeat(timer.currentTimeMs());
       
       if (coordinatorUnknown() && !ensureCoordinatorReady(timer)) {
           return false;
      }

       if (rejoinNeededOrPending()) {
           if (subscriptions.hasPatternSubscription()) {
               
               if (this.metadata.timeToAllowUpdate(timer.currentTimeMs()) == 0) {
                   this.metadata.requestUpdate();
              }

               /*
               在此处会根据当前needFullUpdate needPartialUpdate等状态,
               执行元数据更新逻辑,具体的逻辑则是由ConsumerNetworkClient#poll方法执行。
               */
               if (!client.ensureFreshMetadata(timer)) {
                   return false;
              }

               maybeUpdateSubscriptionMetadata();
          }

           if (!ensureActiveGroup(waitForJoinGroup ? timer : time.timer(0L))) {
               return false;
          }
      }
  } else {
       if (metadata.updateRequested() && !client.hasReadyNodes(timer.currentTimeMs())) {
           client.awaitMetadataUpdate(timer);
      }
  }

   maybeAutoCommitOffsetsAsync(timer.currentTimeMs());
   return true;
}

ConsumerNetworkClient#poll() 封装所有网络通信

ConsumerNetworkClient封装了 consumer和 kafka cluster 之间所有的网络通信的实现,完全异步实现、没有自己维护线程。其中:

  • 所有待发送请求Request都会被存放到ConsumerNetworkClient的成员变量UnsentRequests unsent中。

    • 注:UnsentRequests 是用ConcurrentMap<Node, ConcurrentLinkedQueue<ClientRequest>> unsent定义一个Map来保存所有待发送请求,作为关键字的Node中包含了kafka节点的id、host、port、rack等信息。
    • 返回的Response将存放在ConsumerNetworkClient的成员变量pendingCompletion 中。

每次调用ConsumerNetworkClient# poll() 方法的时候,在当前线程(而不是ConsumerNetworkClient自己维护线程)中发送所有待发送的 Request,处理所有收到的 Response。

这种异步设计的优劣:

  • 优点:占用线程少,吞吐量高
  • 不足:增加代码复杂度
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
public void poll(Timer timer, PollCondition pollCondition, boolean disableWakeup) {
       // 调用handler
       firePendingCompletedRequests();

       lock.lock();
       try {
           // Handle async disconnects prior to attempting any sends
           handlePendingDisconnects();

           /*
           遍历unsent中已保存的所有请求,并调用KafkaClient#send,发送请求
           */
           long pollDelayMs = trySend(timer.currentTimeMs());

           if (pendingCompletion.isEmpty() && (pollCondition == null || pollCondition.shouldBlock())) {
               // if there are no requests in flight, do not block longer than the retry backoff
               long pollTimeout = Math.min(timer.remainingMs(), pollDelayMs);
               /*
               KafkaClient中维护着一个在途请求映射表,参见InFlightRequests类。NetworkClient#send方法
               在发送网络请求时,将会在在途请求映射表加入一条记录。注意是为了避免请求超时导致请求一直等待、
               占用内存、进而导致内存不足
               */
               if (client.inFlightRequestCount() == 0)
                   pollTimeout = Math.min(pollTimeout, retryBackoffMs);
               client.poll(pollTimeout, timer.currentTimeMs());
          } else {
               client.poll(0, timer.currentTimeMs());
          }
           timer.update();

           checkDisconnects(timer.currentTimeMs());
           if (!disableWakeup) {
               maybeTriggerWakeup();
          }
           
           maybeThrowInterruptException();

           trySend(timer.currentTimeMs());

           failExpiredRequests(timer.currentTimeMs());
//清理unsent
           unsent.clean();
      } finally {
           lock.unlock();
      }

       // called without the lock to avoid deadlock potential if handlers need to acquire locks
       firePendingCompletedRequests();

       metadata.maybeThrowAnyException();
  }

KafkaConsumer#updateFetchPositions() 更新消费位置

Kafka Consumer 在消费过程中是需要维护消费位置的,Consumer 每次从当前消费位置拉取一批消息,这些消息都被正常消费后,Consumer 会给 Coordinator 发一个提交位置的请求,然后消费位置会向后移动,完成一批消费过程。

而consumer发起提交位置的请求,还是在updateAssignmentMetadataIfNeeded()方法中,毕竟位置信息也是服务端broker的一个元数据。在`updateAssignmentMetadataIfNeeded方法实现中,最后一句调用KafkaConsumer#updateFetchPositions(timer);,而这个方法又会调用coordinator.refreshCommittedOffsetsIfNeeded()方法。调用链路如下:

注:同一个类内部的方法调用,用类名头部加了’’’区分了下,主要是为了在Typora里画图方便。不支持可参考images/kafka-consumer-refresh-position-sequence-diagram.png 图片。

pollForFetchs() 拉取消息

源码如下

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
private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollForFetches(Timer timer) {
// 省略部分代码        
   // 如果缓存里面有未读取的消息,直接返回这些消息
   final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = fetcher.fetchedRecords();
   if (!records.isEmpty()) {
       return records;
  }

/*
构造拉取消息请求,并发送。
注意,该方法内部,将调用ConsumerNetworkClient#send方法,而这个send方法将会把
kafka节点信息 --> 请求 存放到ConsumerNetworkClient#unsent成员变量中
*/
   fetcher.sendFetches();

   // 省略部分代码
   // 发送网络请求拉取消息,等待直到有消息返回或者超时
   client.poll(pollTimer, () -> {
       // since a fetch might be completed by the background thread, we need this poll condition
       // to ensure that we do not block unnecessarily in poll()
       return !fetcher.hasAvailableFetches();
  });
//省略部分代码
   //返回拉取到的消息
   return fetcher.fetchedRecords();
}

主要逻辑如下:

  1. 如果缓存里面有未读取的消息,直接返回这些消息;

  2. 构造拉取消息请求,并发送;

  3. 发送网络请求并拉取消息,等待直到有消息返回或者超时;

  4. 返回拉到的消息。

    1. fetcher.fetchedRecords会将返回的Response反序列化,返回给调用者。

具体发送操作由fetcher.sendFetches();实现,主要包括如下步骤:

  1. 构造拉取消息请求Request对象

  2. 调用ConsumerNetworkClient#send方法异步发送Request

    1. 这个send方法将会把kafka节点信息 –> 请求 存放到ConsumerNetworkClient#unsent成员变量中
  3. 注册一个回调类来处理返回的Response

    1. Response暂时被存放在Fetcher#completedFetches成员变量中

当调用ConsumerNetworkClient#poll方法时,才会遍历unsent,将其中的请求发出去,并处理收到的Response。

KafkaConsumer相关类图

上图引自:极客时间 消息队列高手课

源码分析之Netty线程模型

源码分析之Netty线程模型

不得不说的Reactor模式

提到Netty,就必须先说一下Reactor模式,源头应该是Doug Lea大神(学java的如果不知道这位神的请自己反思一下……)的Scalable IO in Java所提出的Multiple Reactors模式,参见下图

如果想知道为何出现Reactor模式,需要将IO发展过程,都说一下可能才会比较清晰,此处就不一一展开,有兴趣的童鞋可以参考这篇帖子:Reactor模式

那么,什么是Reactor模式呢?

Reactor模式是事件驱动模型,有一个或多个并发输入源,有一个Service Handler,有多个Request Handlers;这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理。

该图为Reactor模型类图,实现Reactor模式需要实现以下几个类:

1)EventHandler:事件处理器,可以根据事件的不同状态创建处理不同状态的处理器;

2)Handle:可以理解为事件,在网络编程中就是一个Socket,在数据库操作中就是一个DBConnection;

3)InitiationDispatcher:用于管理EventHandler,分发event的容器,也是一个事件处理调度器,Tomcat的Dispatcher就是一个很好的实现,用于接收到网络请求后进行第一步的任务分发,分发给相应的处理器去异步处理,来保证吞吐量;

4)Demultiplexer:阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装,当Selector.select()返回时,可以调用Selector的selectedKeys()方法获取Set,一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。

Reactor时序图 如下:

1)初始化InitiationDispatcher,并初始化一个Handle到EventHandler的Map。

2)注册EventHandler到InitiationDispatcher中,每个EventHandler包含对相应Handle的引用,从而建立Handle到EventHandler的映射(Map)。

3)调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中,调用select()方法(Synchronous Event Demultiplexer)阻塞等待Event发生。

4)当某个或某些Handle的Event发生后,select()方法返回,InitiationDispatcher根据返回的Handle找到注册的EventHandler,并回调该EventHandler的handle_events()方法。

5)在EventHandler的handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler,比如对AcceptorEventHandler来,当有新的client连接时,它会产生新的EventHandler以处理新的连接,并注册到InitiationDispatcher中。

引自:https://www.jianshu.com/p/188ef8462100

Reactor的程序实现

1
2
3
4
5
6
7
8
9
10
11
12
13

void Reactor::handle_events(){
//通过同步事件多路选择器提供的
//select()方法监听网络事件
select(handlers);
//处理网络事件
for(h in handlers){
  h.handle_event();
}
}
// 在主程序中启动事件循环
while (true) {
handle_events();

Netty中的线程模型

为了实现高性能的IO,Netty的线程模型参考了Reactor模式,但没有完全照搬。

Netty 中最核心的概念是事件循环(EventLoop),其实也就是 Reactor 模式中的 Reactor,负责监听网络事件并调用事件处理器进行处理。程序实现上,EventLoop其实就是指上一小节中的整个while(true)循环

在4.x版本Netty中:

  • 网络连接 –> EventLoop :稳定的 n:1关系,这里的稳定指的是关系一旦确定就不再发生变化。

  • EventLoop –> Java线程 : 1:1关系

  • 所以一个网络连接只会对应一个Java线程

    • 最大的好处就是对于一个网络连接的事件处理是单线程的,这样就避免了各种并发问题。

顺便提一句,Java线程与linux操作系统的线程是1:1关系,这个可以参考经典的 The C10K Problem那篇文章。

Netty线程模型可以参考这张图

引自:极客时间-《Java并发编程实战》

如图,Netty中,EventLoopGroup由一组 EventLoop 组成。实际使用中,一般都会创建两个 EventLoopGroup,一个称为 bossGroup,一个称为 workerGroup。

之所以会有2个EventLoopGroup,原因在于socket处理网络请求的机制。socket 处理 TCP 网络连接请求,是在一个独立的 socket 中,每当有一个 TCP 连接成功建立,都会创建一个新的 socket,之后对 TCP 连接的读写都是由新创建处理的 socket 完成的。也就是说处理 TCP 连接请求和读写请求是通过两个不同的 socket 完成的。

在 Netty 中,bossGroup 就用来处理连接请求的,而 workerGroup 是用来处理读写请求的。bossGroup 处理完连接请求后,会将这个连接提交给 workerGroup 来处理, workerGroup 里面有多个 EventLoop,新的连接会交给哪个 EventLoop 来处理由负载均衡算法决定,目前Netty使用的是轮询算法

题外话

Netty 3.x与Netty 4.x 区别较大,有兴趣可以参考这篇文章:netty3与netty4的区别

使用Netty实现Echo服务

下面给出一个具体例子,来看看实际编码是怎么用Netty的。

学习编程语言时,我们往往会先写个HelloWorld,而在网络通信里,这个“HelloWorld”程序就是Echo服务,也就是A发送任意一个消息给B,B将原封不动的把这条消息返回给A。

学习如何使用Netty,最典型、最权威的当属Netty自己提供的范例,我们可以在Netty源码中找到这些例子,参见https://github.com/netty/netty项目中的example文件夹。

注:一般比较完备的开源项目,都会在代码中给出使用范例,至少有单元测试供大家学习参考。

关键点

  • 如果 NettybossGroup 只监听一个端口,那 bossGroup 只需要 1 个 EventLoop 就可以了,多了纯属浪费。
  • 默认情况下,Netty 会创建“2*CPU 核数”个 EventLoop,由于网络连接与 EventLoop 有稳定的关系,所以事件处理器在处理网络事件的时候是不能有阻塞操作的,否则很容易导致请求大面积超时。如果实在无法避免使用阻塞操作,那可以通过线程池来异步处理。

Server端

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
public final class EchoServer {

   static final boolean SSL = System.getProperty("ssl") != null;
   static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

   public static void main(String[] args) throws Exception {
       // Configure SSL.
       final SslContext sslCtx;
       if (SSL) {
           SelfSignedCertificate ssc = new SelfSignedCertificate();
           sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
      } else {
           sslCtx = null;
      }

       // Configure the server.
       EventLoopGroup bossGroup = new NioEventLoopGroup(1);
       EventLoopGroup workerGroup = new NioEventLoopGroup();
       final EchoServerHandler serverHandler = new EchoServerHandler();
       try {
           ServerBootstrap b = new ServerBootstrap();
           b.group(bossGroup, workerGroup)
            .channel(NioServerSocketChannel.class)
            .option(ChannelOption.SO_BACKLOG, 100)
            .handler(new LoggingHandler(LogLevel.INFO))
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    if (sslCtx != null) {
                        p.addLast(sslCtx.newHandler(ch.alloc()));
                    }
                    //p.addLast(new LoggingHandler(LogLevel.INFO));
                    p.addLast(serverHandler);
                }
            });

           // Start the server.
           ChannelFuture f = b.bind(PORT).sync();

           // Wait until the server socket is closed.
           f.channel().closeFuture().sync();
      } finally {
           // Shut down all event loops to terminate all threads.
           bossGroup.shutdownGracefully();
           workerGroup.shutdownGracefully();
      }
  }
}

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) {
       ctx.write(msg);
  }

   @Override
   public void channelReadComplete(ChannelHandlerContext ctx) {
       ctx.flush();
  }

   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
       // Close the connection when an exception is raised.
       cause.printStackTrace();
       ctx.close();
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
正常情况下每个 Channel自己的 ChannelPipeline管理的同一个ChannelHandler Class对象的实例都是直接new的一个新实例,也就是原型模式,而不是单例模式。
@Sharable 表示当前handler将被用于多个ChannelPipeline(单例模式)
*/
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) {
       ctx.write(msg);
  }

   @Override
   public void channelReadComplete(ChannelHandlerContext ctx) {
       ctx.flush();
  }

   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
       // Close the connection when an exception is raised.
       cause.printStackTrace();
       ctx.close();
  }
}

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public final class EchoClient {

   static final boolean SSL = System.getProperty("ssl") != null;
   static final String HOST = System.getProperty("host", "127.0.0.1");
   static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
   static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));

   public static void main(String[] args) throws Exception {
       // Configure SSL.git
       final SslContext sslCtx;
       if (SSL) {
           sslCtx = SslContextBuilder.forClient()
              .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
      } else {
           sslCtx = null;
      }

       // Configure the client.
       EventLoopGroup group = new NioEventLoopGroup();
       try {
           Bootstrap b = new Bootstrap();
           b.group(group)
            .channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline p = ch.pipeline();
                    if (sslCtx != null) {
                        p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                    }
                    //p.addLast(new LoggingHandler(LogLevel.INFO));
                    p.addLast(new EchoClientHandler());
                }
            });

           // Start the client.
           ChannelFuture f = b.connect(HOST, PORT).sync();

           // Wait until the connection is closed.
           f.channel().closeFuture().sync();
      } finally {
           // Shut down the event loop to terminate all threads.
           group.shutdownGracefully();
      }
  }
}
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
public class EchoClientHandler extends ChannelInboundHandlerAdapter {

   private final ByteBuf firstMessage;

   /**
    * Creates a client-side handler.
    */
   public EchoClientHandler() {
       firstMessage = Unpooled.buffer(EchoClient.SIZE);
       for (int i = 0; i < firstMessage.capacity(); i ++) {
           firstMessage.writeByte((byte) i);
      }
  }

   @Override
   public void channelActive(ChannelHandlerContext ctx) {
       ctx.writeAndFlush(firstMessage);
  }

   @Override
   public void channelRead(ChannelHandlerContext ctx, Object msg) {
       ctx.write(msg);
  }

   @Override
   public void channelReadComplete(ChannelHandlerContext ctx) {
      ctx.flush();
  }

   @Override
   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
       // Close the connection when an exception is raised.
       cause.printStackTrace();
       ctx.close();
  }
}

参考资料

Guava RateLimiter 的使用与源码分析(令牌桶算法实现思路)

Guava RateLimiter 的使用与源码分析(令牌桶算法实现思路)

Guava RateLimiter基本使用

学东西时我们应该尽量去看官网、看源码、看官方给出的单元测试。

比如Guava RateLimiter,从RateLimiter类的源码注释中可以看到,官方给出的典型应用场景与使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
As an example, imagine that we have a list of tasks to execute, but we don't want to submit more than 2 per second:

final RateLimiter rateLimiter = RateLimiter.create(2.0); // rate is "2 permits per second"
void submitTasks(List<Runnable> tasks, Executor executor) {
  for (Runnable task : tasks) {
    rateLimiter.acquire(); // may wait
    executor.execute(task);
  }
}

As another example, imagine that we produce a stream of data, and we want to cap it at 5kb per second. This could be accomplished by requiring a permit per byte, and specifying a rate of 5000 permits per second:

final RateLimiter rateLimiter = RateLimiter.create(5000.0); // rate = 5000 permits per second
void submitPacket(byte[] packet) {
  rateLimiter.acquire(packet.length);
  networkService.send(packet);
}

一个是限制执行任务的数量,一个是限制每次发送的字节数量。注意,如果超过RateLimiter.create所容许的permits数量,acquire方法将阻塞,直到产生新的permit。当然,如果不想一直阻塞,可以使用tryAcquire(Duration timeout),该方法,指定一个超时时间,一旦判断出在timeout时间内还无法取得令牌,就返回false。

此外RateLimiter还支持预热功能,预热后缓存能支持 5 万 TPS 的并发,但是在预热前 5 万 TPS 的并发直接就把缓存击垮。

原理

Guava RateLimiter采用的是令牌桶算法。

经典的令牌桶算法描述如下:

1
2
3
1. 令牌以固定的速率添加到令牌桶中,假设限流的速率是 r/ 秒,则令牌每 1/r 秒会添加一个;
2. 假设令牌桶的容量是 b (burst,限流器容许的最大突发流量),如果令牌桶已满,则新的令牌会被丢弃;
3. 请求能够通过限流器的前提是令牌桶中有令牌。

如何用Java实现?

直观思路:生产者-消费者模式,生产者线程定时向阻塞队列添加令牌,被限流的线程作为消费者从阻塞队列获取令牌。

但这种实现的问题:定时器。高并发场景下,当系统压力已经临近极限的时候,定时器的精度误差会非常大,同时定时器本身会创建调度线程,也会对系统的性能产生影响。

Guava如何实现令牌桶算法?

核心思想:记录并动态计算下一令牌发放的时间。

基本思路:

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

class SimpleLimiter {
/*
当前令牌桶中的令牌数量。
notes:注意此处的令牌数量是从当前时刻往后开始计算。但是这个变量只有在消费者线程调用acquire()方法时才会更新。这将导致在调用acquire()方法时,storedPermits是滞后的,需要先更新该值。所以有了resync方法
*/
long storedPermits = 0;
//令牌桶的容量
long maxPermits = 3;
//下一令牌产生时间
long next = System.nanoTime();
//发放令牌间隔:纳秒
long interval = 1000_000_000;
 
//请求时间在下一令牌产生时间之后,则
// 1.重新计算令牌桶中的令牌数
// 2.将下一个令牌发放时间重置为当前时间。
//notes:因为此时已经更新了令牌桶中已有令牌数量,必须将next更新
void resync(long now) {
  if (now > next) {
    //新产生的令牌数
    long newPermits=(now-next)/interval;
    //新令牌增加到令牌桶,由于桶的容量有限,通过取两者最小值来丢弃掉多出来的令牌
    storedPermits=min(maxPermits,
      storedPermits + newPermits);
    //将下一个令牌发放时间重置为当前时间。
    next = now;
  }
}
//预占令牌,返回能够获取令牌的时间
synchronized long reserve(long now){
//notes:先更新当前桶中的令牌数量、下一个令牌的发放时间。如果令牌桶中已有令牌够用,此时resync方法中将执行next=now,也就是立即发放令牌。
//如果令牌不够用、需要等待,那么需要等到next时间。
  resync(now);
  //能够获取令牌的时间
  long at = next;
  //令牌桶中能提供的令牌。本身只需要1个令牌,但需要先看令牌桶中的数量storedPermits是否够用。
  long fb=min(1, storedPermits);
  //令牌净需求:首先减掉令牌桶中的令牌
  /*
  两种情况:
1. 令牌桶已经有足够的令牌:storedPermits >= 1, fb=1, 此时nr=0, 下一个令牌产生时间 next = next,而通过上面可知此时的next就是当前时间now,接着剩余令牌数量减一、更新库存(this.storedPermits -= fb;)
2. 令牌桶内令牌不够: <= storedPermits < 1, fb=storedPermits, 此时 0 < nr <= 1, nr是接下来还需要产生的令牌数量(需要平滑产生),next = next + nr*interval, 就是说等待nr*interval时间之后,令牌桶中将产生1个完整的令牌可供调用。

  */
  long nr = 1 - fb;
  //重新计算下一令牌产生时间
  next = next + nr*interval;
  //重新计算令牌桶中的令牌
  this.storedPermits -= fb;
  //返回本次调用获取令牌的时间
  return at;
}
//申请令牌
void acquire() {
  //申请令牌时的时间
  long now = System.nanoTime();
  //预占令牌
  long at=reserve(now);
  /*
  还是要分两种情况:
  1. 令牌桶已经有足够的令牌: 此时at=now,waitTime=0,获得令牌,直接返回
  2. 令牌桶内令牌不够: 此时at=下次产生令牌时间,需要等待
  */
  long waitTime=max(at-now, 0);
  //按照条件等待
  if(waitTime > 0) {
    try {
      TimeUnit.NANOSECONDS
        .sleep(waitTime);
    }catch(InterruptedException e){
      e.printStackTrace();
    }
  }
}
}

有关算法的简要解释:

next 变量的意思是下一个令牌的生成时间,可以理解为当前线程请求的令牌的生成时刻,如第一张图所示:线程 T1 的令牌的生成时刻是第三秒。

线程 T 请求时,存在三种场景:

  1. 桶里有剩余令牌。
  2. 刚创建令牌,线程同时请求。
  3. 桶里无剩余令牌。

场景 2 可以想象成线程请求的同时令牌刚好生成,没来得及放入桶内就被线程 T 拿走了。因此将场景 2 和场景 3 合并成一种情况,那就是桶里没令牌。即线程请求时,桶里可分为有令牌和没令牌。

“桶里没令牌”,线程 T 需要等待;需要等待则意味着 now(线程 T 请求时刻) 小于等于 next(线程 T 所需的令牌的生成时刻)。这里可以想象一下线程 T 在苦苦等待令牌生成的场景,只要线程 T 等待那么久之后,就会被放行。放行这一刻令牌同时生成,立马被线程拿走,令牌没放入桶里。对应到代码就是 resync 方法没有进入 if 语句内。

“桶里有令牌”,线程 T 不需要等待。说明线程 T 对应的令牌已经早早生成,已在桶内。代码就是:now > next(请求时刻大于对应令牌的生成时刻)。因此在分配令牌给线程之前,需要计算线程 T 迟到了多久,迟到的这段时间,有多少个令牌生成¹;然后放入桶内,满了则丢弃²;未来的线程的令牌在这个时刻已经生成放入桶内³(即 resync 方法的逻辑)。线程无需等待,所以不需要增加一个 interval 了。

角标分别对应 resync 方法内的代码: ¹: long newPermits=(now-next)/interval; ²: storedPermits=min(maxPermits, storedPermits + newPermits); ³: next = now;

对于reverse方法,首先肯定是计算令牌桶里面的令牌数量,然后取令牌桶中的令牌数量storedPermits 与当前的需要的令牌数量 1 做比较,大于等于 1,说明令牌桶至少有一个令牌,此时下一令牌的获取是不需要等待的,表现为 next 不需要变化;而当令牌桶中的令牌没有了即storedPermits等于 0 时,next 就会变化为下一个令牌的获取时间,注意 nr 的值变化

参考资料

  • java并发编程实战 专栏课

tomcat和dubbo对于JDK线程池的修改

tomcat和dubbo对于JDK线程池的修改

预备知识

  • 计算任务的分类

    • CPU密集型:需要线程长时间进行的复杂的运算,这种类型的任务需要少创建线程,过多的线程将会频繁引起上文切换,降低任务处理处理速度。
    • IO密集型:由于线程并不是一直在运行,可能大部分时间在等待 IO 读取/写入数据,增加线程数量可以提高并发度,尽可能多处理任务。
  • JDK线程池,java.util.concurrent.ThreadPoolExecutor 传说中的7个参数,作用,线程池运行机制,参见下图复习

概述

结合ThreadPoolExecutor的运行过程,可以知道ThreadPoolExecutor主要倾向于CPU密集型任务,但对于对于 io 密集型任务,如数据库查询,rpc 请求调用等,就不是很友好。

由于 Tomcat/Jetty 需要处理大量客户端请求任务,如果采用原生线程池,一旦接受请求数量大于线程池核心线程数,这些请求就会被放入到队列中,等待核心线程处理。这样做显然降低这些请求总体处理速度,所以两者都没采用 JDK 原生线程池。

Jetty选择自己实现线程池组件,可以定制化开发,但难度较大,而Tomcat选择扩展ThreadPoolExecutor,相对比较简单。

Tomcat线程池源码分析

Tomcat源码中,为扩展线程池,主要修改了:

  • 自定义ThreadPoolExecutor,直接继承JDK的ThreadPoolExecutor,重写部分逻辑
  • 实现TaskQueue,直接继承LinkedBlockingQueue ,重写 offer 方法。

ThreadPoolExecutor

线程池核心方法execute(),Tomcat简单做了修改,还是将工作任务交给父类,也就是Java原生线程池处理,但增加了一个重试策略。如果原生线程池执行拒绝策略的情况,抛出 RejectedExecutionException 异常。这里将会捕获,然后重新再次尝试将任务加入到 TaskQueue ,尽最大可能执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void execute(Runnable command, long timeout, TimeUnit unit) {
       this.submittedCount.incrementAndGet();

       try {
           super.execute(command);
      } catch (RejectedExecutionException var9) {
           if (!(super.getQueue() instanceof TaskQueue)) {
               this.submittedCount.decrementAndGet();
               throw var9;
          }

           TaskQueue queue = (TaskQueue)super.getQueue();

           try {
               //拒绝后,再尝试入队,还不行则抛出异常
               if (!queue.force(command, timeout, unit)) {
                   this.submittedCount.decrementAndGet();
                   throw new RejectedExecutionException(sm.getString("threadPoolExecutor.queueFull"));
              }
          } catch (InterruptedException var8) {
               this.submittedCount.decrementAndGet();
               throw new RejectedExecutionException(var8);
          }
      }
  }

需要注意 submittedCount 变量。这是 Tomcat 线程池内部一个重要的参数,它是一个 AtomicInteger 变量,将会实时统计已经提交到线程池中,但还没有执行结束的任务。也就是说 submittedCount 等于线程池队列中的任务数加上线程池工作线程正在执行的任务。

TaskQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class TaskQueue extends LinkedBlockingQueue<Runnable> {
......
       //tomcat-util-10.0.0-M6.jar
   public boolean offer(Runnable o) {
       if (this.parent == null) {
           //1.若没有给出tomcat线程池对象,则调用父类方法
           return super.offer(o);
      } else if (this.parent.getPoolSize() == this.parent.getMaximumPoolSize()) {
           //2.若当前线程数已达到最大线程数,则放入阻塞队列
           return super.offer(o);
      } else if (this.parent.getSubmittedCount() <= this.parent.getPoolSize()) {
           //3.若当前已提交任务数量小于等于最大线程数,说明此时有空闲线程。此时将任务放入队列中,立刻会有空闲线程来处理该任务
           return super.offer(o);
      } else {
           //4.若当前线程数小于最大线程数,发返回false,此时线程池将会创建新线程!!!
           return this.parent.getPoolSize() < this.parent.getMaximumPoolSize() ? false : super.offer(o);
      }
  }
}

核心在最后一个三元判断中,这里需要结合Java的ThreadPoolExecutor一起看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

public void execute(Runnable command) {
       if (command == null)
           throw new NullPointerException();
       /*
        * Proceed in 3 steps:
        *
        * 1. If fewer than corePoolSize threads are running, try to
        * start a new thread with the given command as its first
        * task. The call to addWorker atomically checks runState and
        * workerCount, and so prevents false alarms that would add
        * threads when it shouldn't, by returning false.
        *
        * 2. If a task can be successfully queued, then we still need
        * to double-check whether we should have added a thread
        * (because existing ones died since last checking) or that
        * the pool shut down since entry into this method. So we
        * recheck state and if necessary roll back the enqueuing if
        * stopped, or start a new thread if there are none.
        *
        * 3. If we cannot queue task, then we try to add a new
        * thread. If it fails, we know we are shut down or saturated
        * and so reject the task.
        */
       int c = ctl.get();
       if (workerCountOf(c) < corePoolSize) {
           if (addWorker(command, true))
               return;
           c = ctl.get();
      }
    //如果阻塞队列返回false,将会走else分支,去创建新的线程
       if (isRunning(c) && workQueue.offer(command)) {
           int recheck = ctl.get();
           if (! isRunning(recheck) && remove(command))
               reject(command);
           else if (workerCountOf(recheck) == 0)
               addWorker(null, false);
      }
       else if (!addWorker(command, false))
           reject(command);
  }

由此,即可看出Tomcat通过扩展的方式改变了线程池运行机制。

Dubbo线程池源码分析

dubbo中也采用了与Tomcat一样的思路去修改Java线程池,可以参考源码中的EagerThreadPool,也是同样的修改:

  • 自定义ThreadPoolExecutor,直接继承JDK的ThreadPoolExecutor,重写部分逻辑
  • 实现TaskQueue,直接继承LinkedBlockingQueue ,重写 offer 方法。

EagerThreadPoolExecutor源码总共就这么一丢丢,重写的逻辑是:

  • execute方法添加一次重试
  • 加上有关submittedTaskCount这个变量的维护,该变量表示当前正在被执行的任务数量
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
public class EagerThreadPoolExecutor extends ThreadPoolExecutor {

   /**
    * task count
    */
   private final AtomicInteger submittedTaskCount = new AtomicInteger(0);

   public EagerThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit, TaskQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
       super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
  }

   /**
    * @return current tasks which are executed
    */
   public int getSubmittedTaskCount() {
       return submittedTaskCount.get();
  }

   @Override
   protected void afterExecute(Runnable r, Throwable t) {
       submittedTaskCount.decrementAndGet();
  }

   @Override
   public void execute(Runnable command) {
       if (command == null) {
           throw new NullPointerException();
      }
       // do not increment in method beforeExecute!
       submittedTaskCount.incrementAndGet();
       try {
           super.execute(command);
      } catch (RejectedExecutionException rx) {
           // retry to offer the task into queue.
           final TaskQueue queue = (TaskQueue) super.getQueue();
           try {
               if (!queue.retryOffer(command, 0, TimeUnit.MILLISECONDS)) {
                   submittedTaskCount.decrementAndGet();
                   throw new RejectedExecutionException("Queue capacity is full.", rx);
              }
          } catch (InterruptedException x) {
               submittedTaskCount.decrementAndGet();
               throw new RejectedExecutionException(x);
          }
      } catch (Throwable t) {
           // decrease any way
           submittedTaskCount.decrementAndGet();
           throw t;
      }
  }
}

TaskQueue同样是继承自LinkedBlockingQueue,也只是改了一丢丢:

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
public class TaskQueue<R extends Runnable> extends LinkedBlockingQueue<Runnable> {

   private static final long serialVersionUID = -2635853580887179627L;

   private EagerThreadPoolExecutor executor;

   public TaskQueue(int capacity) {
       super(capacity);
  }

   public void setExecutor(EagerThreadPoolExecutor exec) {
       executor = exec;
  }

   @Override
   public boolean offer(Runnable runnable) {
       if (executor == null) {
           throw new RejectedExecutionException("The task queue does not have executor!");
      }

       int currentPoolThreadSize = executor.getPoolSize();
       // have free worker. put task into queue to let the worker deal with task.
       if (executor.getSubmittedTaskCount() < currentPoolThreadSize) {
           return super.offer(runnable);
      }

       // return false to let executor create new worker.
       if (currentPoolThreadSize < executor.getMaximumPoolSize()) {
           return false;
      }

       // currentPoolThreadSize >= max
       return super.offer(runnable);
  }

   /**
    * retry offer task
    *
    * @param o task
    * @return offer success or not
    * @throws RejectedExecutionException if executor is terminated.
    */
   public boolean retryOffer(Runnable o, long timeout, TimeUnit unit) throws InterruptedException {
       if (executor.isShutdown()) {
           throw new RejectedExecutionException("Executor is shutdown!");
      }
       return super.offer(o, timeout, unit);
  }
}

重点还是在offer方法的实现,开发者都写了注释,我就不废话了 🙂 真的是和tomcat一毛一样的思路。

收工。

参考资料

解决win 10通过docker安装pinpoint报端口错误

解决win 10通过docker安装pinpoint报端口错误

按官方提示https://github.com/naver/pinpoint-docker ,通过docker安装pinpoint时,遇到端口冲突问题

``` docker-compose up -d

Creating network “pinpoint-docker_pinpoint” with driver “bridge”
Creating pinpoint-docker_zoo3_1 … done
Creating pinpoint-docker_zoo2_1 … done
Creating pinpoint-flink-jobmanager … done
Creating pinpoint-docker_zoo1_1 … done
Creating pinpoint-mysql … done
Creating pinpoint-hbase … error
Creating pinpoint-flink-taskmanager … done

ERROR: for pinpoint-hbase Cannot start service pinpoint-hbase: Ports are not available: listen tcp 0.0.0.0:2180: bind: An attempt was made to access a socket in a way forbidden by its access permissions.

ERROR: for pinpoint-hbase Cannot start service pinpoint-hbase: Ports are not available: listen tcp 0.0.0.0:2180: bind: An attempt was made to access a socket in a way forbidden by its access permissions.
Encountered errors while bringing up the project. ``` |

根据官方提示,修改pinpoint-docker文件夹下的.env文件,修改hbase端口即可,比如我修改为了12180

``` ### Pinpoint-Hbase

PINPOINT_HBASE_NAME=pinpoint-hbase
#config for hbase in external docker
EXTERNAL_HBASE_PORT=12180 ``` |

写给即将或是想进入互联网大厂工作的后浪们

写给即将或是想进入互联网大厂工作的后浪们

明确目标

进入互联网行业,不管是实习,还是正式工作,要记住,从入行这一刻起,就要不停的学习、不停的思考,最好是根据自己的优缺点、喜好,订个明确的目标,自己到底想走哪条路线,是纯技术路线、不想太关心业务?产品经理?程序员只是过渡、想走管理?还是在互联网公司捞几年钱然后就转行?

  • 对技术特别感兴趣:那毫无疑问,技术这条路,其实也没有上限,你可以去极客时间app上看看那些大牛们

制定计划、逐步实行

听起来有点土、老生常谈了对吗?但我觉得这个当真是最朴实、最简单、也最实用的道理。

我目前在某央企、某互联网公司都做过程序员这个岗位。互联网行业,给我最大感触就是变化快、对于程序员的要求也越来越高,各种红利基本已经耗尽,前些年很多人看程序员赚钱、纷纷转行做程序员,这个恐怕后续会越来越难,从面试所考察内容就有直观感受,虽然每年大家都会说“互联网寒冬”、公司难,但从2019年年末开始,明显考察的内容越来越深、难。比如有如下一些题目(下面主要以java程序员举例,毕竟需求量大):

  • java实现单例模式有哪些?知道DCL(双重锁检查)吗?为何要这么写?是否需要加volatile?
  • java里synchronized关键字,锁膨胀的过程?
  • java 线程池的7个参数?为何阿里开发规范建议自己创建线程池、不要用JDK工具方法创建?
  • java AQS是什么?用AQS手写一个锁?
  • java里类加载的双亲委托模型?JDBC是如何打破双亲委托模型的?
  • JVM垃圾回收器有哪些?各自算法?怎么搭配?
  • JVM调优?
  • kafka为何速度那么快?
  • 常见MQ有哪些?特点是什么?
  • spring cloud有哪些组件?spring cloud 和spring boot的关系?怎么实现高可用?
  • ……

上述还只是一部分,做一个合格的Java程序员实在太难了……目前就社招而言,想成为一个比较好的java程序员,能够脱颖而出、受大厂青睐的话,一般下面这几方面,至少得会,并且最好有一方面非常精通:

  • java多线程、高并发相关
  • JVM,会调优
  • mysql
  • MQ
  • redis
  • zookeeper

越是薪资高的岗位,要求越高。互联网公司,java程序员需求量很大,但已经不是找个培训班、上1个月学学spring mvc、会CRUD操作就可以胜任工作的时代了。可能实际工作里,确实用不到那么多,哪怕是阿里这种技术公司,同样如此。但“面试造火箭,进去拧螺丝”,那些受青睐的大厂们,就是因为能做那些岗位的人太多、他们需要筛选、优中选优,尽可能找到进来就能立刻干活儿的人,所以面试越来越难。

对于校招生

我的建议就是:

  • 基础一定要扎实

    • 尤其数据结构、算法、操作系统、计算机网络等。校招生刚入职时一般不会安排太重要的工作(大牛学弟学妹们除外,我只是说多数情况),所以大家可能参加工作后的前两年以为这些课不重要。但越往后,越是想往程序员这条(不归)路上深入走,就会发现这些课实在太重要了。那些大部头的书(类似操作系统第6版、CSAPP、重构这种书),早晚还是得仔细看。我目前在翻的书大家可以体会一下:

      • 深入理解JVM 周志明,第三版(一块砖头……)
      • 高性能Mysql 第三版(又一块砖头……)
      • redis设计与实现 (计划中、暂时还没时间看的一块砖头……)
      • java并发编程的艺术 方腾飞
      • zookeeper 分布式过程协同技术详解
  • 一定要看源码

    • 作为校招生,应该试着去看JDK 源码(比如HashMap ConcurrentHashMap AQS等),spring 源码可以尝试一下,这两个都是设计比较好的,源码中往往有很多注释,可以让你了解作者为何这样/那样实现,对于你自己的编程能力提高很有帮助。同时,这个对于校招有加分。
    • 等到社招时,基本看源码是必问的,到时候会更深,比如,大家应该听说过netty,这玩意如果你看过源码、特别清楚原理,恐怕过面试so easy。只是举例子,netty这个估计对于在校生有些难,建议从JDK spring开始,养成读源码的习惯。
  • 想进大厂,务必刷刷算法题

    • 推荐极客时间的付费专栏《数据结构和算法之美》并尽量按作者提到的这个目录 https://github.com/wangzheng0822/algo 自己实现一下。

    • 算法题,还是得刷刷leetcode,可以参与leetcode-cn每天刷题打卡的活动,持之以恒。

    • B站 花花酱 刷题视频(我没看,但B站上貌似看的人很多),或是覃超大魔王 五毒神掌刷题法(他的这个刷题法在B站有公开视频,我觉得不错,有按照他的思路在练习,有条件的可以参加下极客时间 算法训练营,不过网上有流出视频,可以找找)

    • labuladong 算法小抄,这个强烈推荐,有算法模板、习题分析 https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie 有公众号labuladong

    • 最重要的是务必自己去练手、不要以为看题解就理解了,要不断、反复练习,一道题刷几遍,简单到不能再简单(比如二叉树按层遍历),可以不用再练,但只要还有不理解、再次遇到时没法在5min内有思路的leetcode题目,都要隔段时间练习的。

  • 加入一些技术群/刷题群,看大家是怎么学的

    • 学习是一件反人类的事,很容易懈怠。加群,多看看网上有多少人在一起学习,可以方便相互交流,同时相互激励、避免自己偷懒。
  • 持续学习,有输出

    • 如果是学编程语言、各种框架,必须自己手写demo、跑程序、看看效果。最好养成学东西时尽量写笔记/博客的习惯,这样逐步积累,经年累月下来会非常可观。
  • 找项目练手

    • 网上,github上可以练手的项目很多,这个可以根据自身情况,自己找一下,一般培训机构都会选择网上商城,因为涉及的业务场景复杂、各种框架、中间件用的比较多,这里就不好推荐哪个了。
  • 极客时间

    • 极客时间的内容做的不错,每天听听卖桃者说,听听大牛们的观点,有条件的根据自己兴趣买些课学习,上面的课有不少质量相当高,以下仅推荐(我不是带货的,是真的不错的专栏哦),这些大佬们讲课往往是实际项目经验+理论知识,有些甚至能拿来直接用、解决工作中的问题:

      • 陈皓 《左耳听风》
      • 王争 《数据结构和算法之美》
      • 丁奇 《mysql 45讲》
      • 李玥 《消息队列高手课》
  • 谷歌评分卡

    • 技术的道路很长,可以参考谷歌评分卡,给自己打打分

    • 以下翻译取自:https://www.jianshu.com/p/b1f57417320d

      0 – you are unfamiliar with the subject area.(0 -你不熟悉主题领域。)

      1 – you can read / understand the most fundamental aspects of the subject area.(1 -你可以阅读/了解主题领域最基本的方面。)

      2 – ability to implement small changes, understand basic principles and able to figure out additional details with minimal help.(2 -能够实现小的变化,理解基本原理,并能在最小的帮助下找出更多的细节。)

      3 – basic proficiency in a subject area without relying on help.(3 -在不依赖帮助的情况下,熟练掌握某一科目。)

      4 – you are comfortable with the subject area and all routine work on it: (4 -你对主题领域和所有日常工作都很熟悉:) For software areas – ability to develop medium programs using all basic language features w/o book, awareness of more esoteric features (with book).(对于软件领域来说,能够使用所有基本的语言来开发中等的程序,使用w/o book,了解更深奥的特性(带书)。) For systems areas – understanding of many fundamentals of networking and systems administration, ability to run a small network of systems including recovery, debugging and nontrivial troubleshooting that relies on the knowledge of internals.(对于系统领域——了解网络和系统管理的许多基础知识,能够运行一个小型的系统网络,包括恢复、调试和依赖于内部知识的重要故障排除。)

      5 – an even lower degree of reliance on reference materials. Deeper skills in a field or specific technology in the subject area.(5 -对参考资料的依赖程度更低。在某一领域或某一特定技术领域有较深的技能。)

      6 – ability to develop large programs and systems from scratch. Understanding of low level details and internals. Ability to design / deploy most large, distributed systems from scratch.(6 -能够从头开始开发大型程序和系统。了解低层次的细节和内部信息。能够设计/部署大多数大型的分布式系统。)

      7 – you understand and make use of most lesser known language features, technologies, and associated internals. Ability to automate significant amounts of systems administration.(7 -你理解并利用最不知名的语言特征、技术和相关的内部信息。能够自动化大量的系统管理。)

      8 – deep understanding of corner cases, esoteric features, protocols and systems including “theory of operation”. Demonstrated ability to design, deploy and own very critical or large infrastructure, build accompanying automation.(8 -深刻理解角落案例,深奥的特点,协议和系统,包括“操作理论”。演示了设计、部署和拥有非常关键或大型基础设施的能力,并建立了相应的自动化。)

      9 – could have written the book about the subject area but didn’t; works with standards committees on defining new standards and methodologies.(9 -本可以写关于主题领域的书,但没有;与标准委员会一起制定新的标准和方法。)

      10 – wrote the book on the subject area (there actually has to be a book). Recognized industry expert in the field, might have invented it.(10 -写在主题领域的书(实际上必须有一本书)。业内公认的业内专家,可能已经发明了它。)

      Subject Areas:

      TCP/IP Networking (OSI stack, DNS etc)

      Unix/Linux internals

      Unix/Linux Systems administration

      Algorithms and Data Structures

      C

      C++

      Python

      Java

      Perl

      Go

      Shell Scripting (sh, Bash, ksh, csh)

      SQL and/or Database Admin

      Scripting language of your choice (not already mentioned) _

      People Management

      Project Management

Java并发之深入解析volatile关键字

Java并发之深入解析volatile关键字

从一道面试题讲起:采用DCL实现单例模式时,是否需要加volatile关键字?为什么?

有关单例模式

我们在网上搜如何实现单例模式时,帖子往往给出多种实现:饿汉模式、懒汉模式、双重锁懒汉模式(双重锁检查,double check lock,经常简写做DCL)、静态内部类模式、枚举模式等。

此处可以参考 深入理解单例模式:静态内部类单例原理 这篇文章。

顺带提一句,《Java并发编程的艺术》(方腾飞)第3章有讨论过DCL、静态内部类这两种实现方式,建议去读一读。

先说结论:DCL实现单例,必须加volatile!

至于为何,首先我们需要理解volatile的语义。

volatile的语义

目前我们正在使用的Java版本中,对于volatile语义的定义,主要采用 JSR-133中的定义。

Oracle JSR-133文档:英文原版 中文翻译版

JSR-133在对JLS原始规范的改变中,有两处最有可能要求JVM实现也做出相应的变动:

加强了volatile变量的语义,需要有acquire和release语义。在原始的规范中,volatile变量的访问和非volatile变量的访问之间可以自由地重排序。
加强了final字段的语义,无需显式地同步,不可变对象也是线程安全的。这可能需要在给final字段赋值的那些构造器的末尾加上store-store屏障。

volatile的内存语义

CPU缓存级别

happens-before对volatile规则的定义 : volatile变量的写,先发生于后续对这个变量的读.
这句话的含义有两层:
volatile 的写操作, 需要将线程本地内存值,立马刷新到 主内存的共享变量中.
volatile 的读操作, 需要从主内存的共享变量中读取,更新本地内存变量的值.
由此引出 volatile 的内存语义.
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存.
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量,并更新本地内存的值.

注:引自https://www.jianshu.com/p/9e467de97216

通俗一点

简单来讲,目前volatile关键字主要的作用是:

    1. 在多线程访问,保证被volatile修饰变量的可见性
    2. 禁止指令重排序

分析DCL代码

一个典型的DCL代码如下(没加volatile、有问题的版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingleTon{
//没加volatile、有问题的版本
private static SingleTon INSTANCE = null;
private SingleTon(){}
public static SingleTon getInstance(){
if(INSTANCE == null){
synchronized(SingleTon.class){
if(INSTANCE == null){
INSTANCE = new SingleTon();
}
}
return INSTANCE;
}
}
}

有问题地方是这里:

1
INSTANCE = new SingleTon();

这条语句编译成字节码指令后,实际分下面3个步骤:

  1. 在堆内存开辟内存空间。
  2. 在堆内存中实例化SingleTon里面的各个参数。
  3. 把对象指向堆内存空间。

由于JVM存在乱序执行功能,所以可能线程A在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。

在JSR-133(JDK 1.5中实现)之后,官方也发现了这个问题,增强了volatile,只要定义为private volatile static SingleTon INSTANCE = null;就可解决DCL失效问题。volatile确保INSTANCE每次均在主内存中读取,这样虽然会牺牲一点效率,但也无伤大雅。

volatile如何保证可见性并禁止指令重排序?

基础知识

Java程序执行:

JVM执行字节码时,可能发生指令重排序,乱序执行。上面不加volatile的DCL重排就是一个例子。

需要分多个层次来理解:

1. 在Java字节码层面

字节码文件结构

在字段表中保存着类中各种字段的信息

当字段被volatile修饰时,字段表中对应字段的访问标志将会加上ACC_VOLATILE

2. JVM的内存屏障

as-if-serial与happen-before

虽然会乱序执行,但会遵循as-if-serial语义和happen-before原则。

  • as-if-serial : 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不会改变。
  • happen-before :
    • 与程序员密切相关的happens-before规则如下:
      1、程序顺序规则:一个线程中的每个操作,happens-before于线程中的任意后续操作。
      2、监视器锁规则:一个锁的解锁,happens-before于随后对这个锁的加锁。
      3、volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
      4、传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

可以参考 happens-before规则和as-if-serial语义 。不用记各种规则,理解即可。

编译器的重排序是指,在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序来优化程序的性能.

编译器的重排序和CPU的重排序的原则一样,会遵守数据依赖性原则,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序

编译器、处理器都必须遵守这个语义。JMM层面的内存屏障为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理器的重排序。

JVM中内存屏障有以下4种:

  1. LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,
    在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  2. StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,
    在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  3. LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,
    在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  4. StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,
    在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

参考JVM系列(三)[计算机硬件的内存模型,数据一致性问题,CPU指令乱序执行,合并写]

3. Hotspot源码实现

JVM执行字节码的过程,最终会翻译成机器语言,在CPU上执行。

对于JVM内存屏障的实现,不同CPU有不同的实现。以X86为例,下面是个栗子:

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情:

  • 将当前处理器缓存行的数据会写回到系统内存。
  • 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

也就是说用lock指令来保证CPU中缓存的可见性。

在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。

对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据 。

一个处理器的缓存回写到内存会导致其他处理器的缓存无效 。IA-32处理器和Intel 64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。

上述内容参考自:并发之volatile底层原理

总结一下:LOCK 用于在多处理器中执行指令时对共享内存的独占使用。它的作用是能够将当前处理器对应缓存的内容刷新到内存,并使其他处理器对应的缓存失效。另外还提供了有序的指令无法越过这个内存屏障的作用。

Hotspot C++源码分析可以参考这篇文章面试必问的volatile,你了解多少?

3.1 扩展1:缓存行(cache line)

参考 并发之volatile底层原理

3.2 扩展2:缓存一致性协议

参考这篇文章 【并发编程】MESI–CPU缓存一致性协议

参考资料