1. ThreadFactory优雅创建线程
/**
* Name thread factory.
*
* @author <a href="mailto:liaochuntao@live.com">liaochuntao</a>
*/
public class NameThreadFactory implements ThreadFactory {
// 原子类构建线程名称,防止重复
private final AtomicInteger id = new AtomicInteger(0);
// 线程组名称
private String name;
public NameThreadFactory(String name) {
if (!name.endsWith(StringUtils.DOT)) {
name += StringUtils.DOT;
}
this.name = name;
}
@Override
public Thread newThread(Runnable r) {
String threadName = name + id.getAndIncrement();
Thread thread = new Thread(r, threadName);
thread.setDaemon(true);
return thread;
}
}
-----------------------------------------------------------------------------
// 使用
new NameThreadFactory("com.alibaba.nacos.naming.timer")
我们平时都会使用ThreadPoolExecutor来创建线程池。使用较多的是如下构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
这种构建线程池的缺点是我们无法定制线程,无法设置线程名称、是否为守护线程等信息。但是通常在项目中会使用多个线程池,所以为了区分线程,我们往往需要根据业务或者服务来命名线程。所以定制线程就称为了一个必要的需求。
所以,我们可以使用如下的方式,通过ThreadFactory的方式定制线程:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
threadFactory, defaultHandler);
}
我们只需要定义一个类实现接口ThreadFactory接口,然后在newThread方法中定义自己的线程创建逻辑就可以了。
2. 线程池构建
private static final ScheduledExecutorService NAMING_TIMER_EXECUTOR = ExecutorFactory.Managed
.newScheduledExecutorService(ClassUtils.getCanonicalName(NamingApp.class),
EnvUtil.getAvailableProcessors(2), new NameThreadFactory("com.alibaba.nacos.naming.timer"));
3. 设置核心线程算法
public static int getAvailableProcessors(int multiple) {
if (multiple < 1) {
throw new IllegalArgumentException("processors multiple must upper than 1");
}
// 获取配置中的处理器个数
Integer processor = getProperty(Constants.AVAILABLE_PROCESSORS_BASIC, Integer.class);
// 如果获取到处理器个数,则核心线程数为处理器个数*2
// 否则执行getSuitableThreadCount构建核心线程数
return null != processor && processor > 0 ? processor * multiple : ThreadUtils.getSuitableThreadCount(multiple);
}
----------------------------------------------------------------------
public static int getSuitableThreadCount(int threadMultiple) {
final int coreCount = Runtime.getRuntime().availableProcessors();
int workerCount = 1;
while (workerCount < coreCount * threadMultiple) {
workerCount <<= 1;
}
return workerCount;
}
4. 优雅定义任务
我们都知道,线程池提交的任务一般是Runnable接口的子类。我们在实际项目中往往会根据业务,定义出专属的也谢task类。这些的做法是比较好的,因为我们根据task的名称可以很容易的区分出是哪个业务相关的任务。
比如Nacos中的这个Notifier 就实现了Runnable接口。
public class Notifier implements Runnable {
// 定义ConcurrentHashMap来存储服务实例
private ConcurrentHashMap<String, String> services = new ConcurrentHashMap<>(10 * 1024);
// 存储需要注册的服务实例到一个阻塞队列中,然后异步注册服务
private BlockingQueue<Pair<String, DataOperation>> tasks = new ArrayBlockingQueue<>(1024 * 1024);
/**
* Add new notify task to queue.
*
* @param datumKey data key
* @param action action for data
*/
public void addTask(String datumKey, DataOperation action) {
// 如果任务已经存在直接返回
if (services.containsKey(datumKey) && action == DataOperation.CHANGE) {
return;
}
// 如果任务不存在,将其记录到services map中去
if (action == DataOperation.CHANGE) {
services.put(datumKey, StringUtils.EMPTY);
}
// 向阻塞队列添加任务
tasks.offer(Pair.with(datumKey, action));
}
public int getTaskSize() {
return tasks.size();
}
@Override
public void run() {
Loggers.DISTRO.info("distro notifier started");
// 死循环不会一直占用CPU资源,因为当阻塞队列中没有元素的时候,take方法中判断个数为0,会调用condition.await方法阻塞等待。直到有元素被添加进来。
for (; ; ) {
try {
// 从阻塞队列中获取任务
Pair<String, DataOperation> pair = tasks.take();
// 处理任务
handle(pair);
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
private void handle(Pair<String, DataOperation> pair) {
try {
String datumKey = pair.getValue0();
DataOperation action = pair.getValue1();
// 服务map中移除记录
services.remove(datumKey);
int count = 0;
if (!listeners.containsKey(datumKey)) {
return;
}
for (RecordListener listener : listeners.get(datumKey)) {
count++;
try {
if (action == DataOperation.CHANGE) {
listener.onChange(datumKey, dataStore.get(datumKey).value);
continue;
}
if (action == DataOperation.DELETE) {
listener.onDelete(datumKey);
continue;
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] error while notifying listener of key: {}", datumKey, e);
}
}
if (Loggers.DISTRO.isDebugEnabled()) {
Loggers.DISTRO
.debug("[NACOS-DISTRO] datum change notified, key: {}, listener count: {}, action: {}",
datumKey, count, action.name());
}
} catch (Throwable e) {
Loggers.DISTRO.error("[NACOS-DISTRO] Error while handling notifying task", e);
}
}
}
思考:Nacos为何要异步注册服务?
因为注册服务本身是一个较为复杂的过程,如果是同步操作,那么系统启动的时候就需要完成注册,这个事件是非常慢的。
5. 启动线程池
@PostConstruct
public void init() {
GlobalExecutor.submitDistroNotifyTask(notifier);
}
public static void submitDistroNotifyTask(Runnable runnable) {
DISTRO_NOTIFY_EXECUTOR.submit(runnable);
}
6. 写时复制-解决读写并发冲突
对于注册表的读写并发冲突,Nacos中采用的是写时复制策略。即读取数据的时候读取的是原始集合中的数据,但是写数据的时候就会先加锁,然后复制一份数据集合,然后对这个复制后的数据集合进行写操作。完成写操作之后将初始数据集合指向修改后的地址,然后解锁。
Nacos中保存数据的初始集合(分为临时和持久):
源码类:com.alibaba.nacos.naming.core.Cluster
@JsonIgnore
private Set<Instance> persistentInstances = new HashSet<>();
@JsonIgnore
private Set<Instance> ephemeralInstances = new HashSet<>();
public void updateIps(List<Instance> ips, boolean ephemeral) {
// 获取到初始数据(注意,此时并不是复制整个注册表,而是某个cluster下的服务列表)
Set<Instance> toUpdateInstances = ephemeral ? ephemeralInstances : persistentInstances;
// 复制一份数据到map中
HashMap<String, Instance> oldIpMap = new HashMap<>(toUpdateInstances.size());
// 将旧数据存储到这个oldIpMap中去
for (Instance ip : toUpdateInstances) {
oldIpMap.put(ip.getDatumKey(), ip);
}
// ====================================== 数据修改开始 ========================================================
List<Instance> updatedIps = updatedIps(ips, oldIpMap.values());
if (updatedIps.size() > 0) {
for (Instance ip : updatedIps) {
Instance oldIP = oldIpMap.get(ip.getDatumKey());
// do not update the ip validation status of updated ips
// because the checker has the most precise result
// Only when ip is not marked, don't we update the health status of IP:
if (!ip.isMarked()) {
ip.setHealthy(oldIP.isHealthy());
}
if (ip.isHealthy() != oldIP.isHealthy()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} IP-{} {}:{}@{}", getService().getName(),
(ip.isHealthy() ? "ENABLED" : "DISABLED"), ip.getIp(), ip.getPort(), getName());
}
if (ip.getWeight() != oldIP.getWeight()) {
// ip validation status updated
Loggers.EVT_LOG.info("{} {SYNC} {IP-UPDATED} {}->{}", getService().getName(), oldIP, ip);
}
}
}
List<Instance> newIPs = subtract(ips, oldIpMap.values());
if (newIPs.size() > 0) {
Loggers.EVT_LOG
.info("{} {SYNC} {IP-NEW} cluster: {}, new ips size: {}, content: {}", getService().getName(),
getName(), newIPs.size(), newIPs);
for (Instance ip : newIPs) {
HealthCheckStatus.reset(ip);
}
}
List<Instance> deadIPs = subtract(oldIpMap.values(), ips);
if (deadIPs.size() > 0) {
Loggers.EVT_LOG
.info("{} {SYNC} {IP-DEAD} cluster: {}, dead ips size: {}, content: {}", getService().getName(),
getName(), deadIPs.size(), deadIPs);
for (Instance ip : deadIPs) {
HealthCheckStatus.remv(ip);
}
}
// ====================================== 数据修改结束 ========================================================
// 将修改后的数据复制给toUpdateInstances
toUpdateInstances = new HashSet<>(ips);
// 将上面做的执行修改操作后的数据集合赋值给初始数据集合
if (ephemeral) {
ephemeralInstances = toUpdateInstances;
} else {
persistentInstances = toUpdateInstances;
}
}