Dockerize a Java application and deploy to AKS

Overview

Nowadays, many monolithic applications still run on on-premises infrastructure.
On-premises applications require organizations to handle both hardware and software themselves, which necessitates high initial costs and extensive internal IT resources for maintenance.
Cloud environments typically offer greater scalability, reduced management complexity, and a shift from Capital Expenditure (CapEx) to Operational Expenditure (OpEx) models. Public clouds, with various cloud-native services, are popular, and Microsoft Azure is the second most popular cloud provider in the world.
In this article, we will discuss how to migrate a monolithic application to Microsoft Azure.

Pre-migration

Analyze the Base

First thing first, the existing applications/systems/codebases need to be analyzed to get enough information before doing migration. Effective migration begins with thorough analysis:

  • Discovery: Identify both technical requirements and business objectives.
  • Assumptions: Make educated guesses to clarify uncertainties.
  • Dependency Analysis: Understand the interdependencies within the application to determine the sequence of migration tasks.
  • Proposal Adjustment: Refine the migration strategy based on discoveries and analyses.

Refactor Apps for Dockerization

Before migrating to AKS, which offers significant scalability, ensure the application architecture supports running multiple instances

State management

Consider the implications of distributed state management. Methods such as externalized state storage or adopting stateless architecture might be necessary.
For example, a local in memory cache. When it comes into AKS and runs multiple instances, each instance will not able to share the cache. Hence a distributed cache, or cloud native service such as Azure Cache Redis should be considered.

Configuration for multiple environments

Usually application may have multiple configurations for different environment like separating configurations for development, testing, and production.
For example, for Java Springboot application, usually there are multiple yaml files for different environments, and an active profile name should be specified to indicate which configuration file will be use.
When migrating to Azure, there are 3 ways to deal with it:

  • Keep Spring Profiles and Configuration Files

Pros:

  • Minimal changes
  • Easy to understand
  • Allows full use of all Spring-specific features

Cons:

  • Cannot encrypt sensitive data.

  • Require sourcecode update and redeploy for changes

  • Azure App Configuration & Azure Key Vault

Pros:

  • Can update config in runtime without redeploy
  • Leverages managed services for handling configuration and secrets, reducing operational overhead and complexity.

Cons:

  • Requires changes to the application’s configuration management code and practices.

  • Adds a dependency on external services, which may increase complexity for local development and testing scenarios.

  • Potentially higher costs depending on the usage of Azure services.

  • Hybrid Approach (Recommended)

Take advantage of all configuration types: Put normal configuration into Azure Configuration. Put secrets into Azure Key Vault. Keep configuration which don’t require external management in Spring configuration files.

Inter-service Communication

Evaluate and adapt communication pathways between different application components to ensure they operate efficiently in a cloud environment.

Logging and monitoring

Implement centralized logging and monitoring using Azure Monitor and Application Insights to maintain visibility and track application health and performance.

Migrating to Azure

Dockerize applications

Develop Dockerfiles that ensure security and efficiency:

  • Employ non-root users to enhance security.
  • Use multi-stage builds to minimize the final image size.

Create manifest files for Kubernetes

Define the necessary Kubernetes resources such as Deployments, Services, and Ingress Controllers.

  • ConfigMaps and Secrets: Manage application configuration and secrets efficiently.
  • Pod Management: Design manifest files to effectively manage pods with considerations for scaling and health checks.

Setup Azure Kubenetes Service

Choose an AKS Architecture

Choose an appropriate AKS setup considering factors such as cost, security, and compliance requirements.

Create an AKS Cluster with Azure Container Registry

Integrate with Azure Container Registry for secure and efficient container image storage and management

Deploy Application Gateway with Ingress Controller

  • Ingress Configuration: Utilize Application Gateway as an ingress controller to manage external access efficiently.

Create Cert-Manager for AKS

Automate TLS certificate management for Kubernetes applications to enhance security.

CI/CD with Azure DevOps

Establish Continuous Integration and Continuous Deployment pipelines using Azure DevOps. This setup facilitates automation in building, testing, and deploying applications, ensuring that the application remains in a deployable state after every change.

Post-Migration Considerations:

Disaster Recovery

Implement robust disaster recovery strategies to ensure high availability and data integrity.

Cost optimization

Regularly monitor and optimize cloud spending to ensure efficient resource use.

Performance tuning

Continuously monitor application performance and adjust resources and configurations as needed to meet performance targets.

Conclusion

Migrating to Azure AKS offers significant benefits in scalability and management for monolithic applications. However, it requires careful planning, execution, and ongoing adjustment. By embracing Azure’s native capabilities and ensuring rigorous implementation and optimization practices, organizations can maximize the benefits of cloud migration.

道具合成

设计理念

我们的目标是设计一个直观的制作系统,以玩家背包中的物品作为输入原材料,制造产生新的物品。
制作系统的核心是一个简单的“配方,即需要特定数量的背包中的物品来制作一个物品。我们的实现涉及几个关键组件:

原材料

假设玩家收集的物品存储在背包中。每个物品都有一系列的属性,比如名称、数量、附加属性等。
对于一个简单的合成系统,这里我们只关注物品名称和数量,即有哪些东西,有多少。

制作规则

即配方,它说明了多少个什么东西,能合成多少个什么新东西,它需要包含的信息有:

  • 所需物品列表及其数量。
  • 制作完成后给予的新物品数量。

制作过程

即制作物品,需要干些什么,主要有以下这些步骤:

  1. 验证是否可以制作物品: 检查用来合成的原材料物品是否存在以及库存是否有足够。
  2. 消耗资源: 从背包库存中扣除所需数量的物品。
  3. 创建新物品: 将制作的物品添加回背包。

一个可用的代码片段:

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
let itemsInBag = []
var craftingRules = {
    plant: {
        requires: [
            { name: "flower", qty: 3 },
            { name: "wood", qty: 4 }
        ],
        gives: 1 // Quantity of new item produced
    }
};
function calculateItemsQtyInBag(){
    let result = {}
    for(var item in itemsInBag){
        result[item.id] = item.hold;
    }    
    return result;
}

function make(){
    let itemHold = calculateItemsQtyInBag();
   
}

function craftItem(itemName) {
    if (craftingRules.hasOwnProperty(itemName)) {
        var recipe = craftingRules[itemName];

        // Check if all required items are in inventory in sufficient quantities
        var canCraft = recipe.requires.every(function(requirement) {
            var item = inventory.find(function(item) {
                return item.name === requirement.name;
            });

            return item && item.qty >= requirement.qty;
        });

        if (canCraft) {
            // Deduct required items from inventory
            recipe.requires.forEach(function(requirement) {
                var item = inventory.find(function(item) {
                    return item.name === requirement.name;
                });

                if (item) {
                    item.qty -= requirement.qty;
                }
            });

            // Add new item to inventory
            var existingItem = inventory.find(function(item) {
                return item.name === itemName;
            });

            if (existingItem) {
                existingItem.qty += recipe.gives;
            } else {
                inventory.push({ id: new Date().getTime(), name: itemName, qty: recipe.gives });
            }

            console.log(itemName + " crafted successfully!");
            return true;
        } else {
            console.log("Not enough resources to craft " + itemName);
            return false;
        }
    } else {
        console.log("No such item exists to craft");
        return false;
    }

BMW - Application Architect

Summary: 甲方国内公司技术面试,技术面2轮,这是第一轮涉及的一些东西,
面试的职位是Java application/module architect, 主要问的是一些微服务相关的常用设计,以及适用场景,具体如下图。
BMW - Application Architect.png

Current Position - Senior Software Engineer

Senior Java Engineer技术面试

面试时间: 90分钟
形式: 线上面试
面试结果:通过/拿到Offer

由于我还在这儿上班~我就不说是哪家公司了
整个流程为HR沟通->技术面试->成都Office Engineer head面试-> Engineer Head offer沟通。
Offer薪资范围参考招聘软件上的最高的那个范围。固定13薪+project/personal bonus, 但2种bonus说是可以忽略不计。
谈薪资时表示在上家的base基础上涨20%是很合理的,我开口就答应了,感觉我要低了。
面试中除了HR之外,所有面试上来就是英文,基本看英文ok之后会切换中文。
技术面试计划1小时,实际使用1小时47分钟。
面试深度不深,但广度较广,包含Java基础至微服务、分布式所有常见问题,以及一些敏捷、工程管理方法论,和实操经验,最后有一道在线编程题。

以下是我回忆的大致内容,用什么语言问的就用什么语言写的。

Q1: Could you talk about difference btw ArrayList and LinkedList

ArrayList is an array. It is fast for random visit because it has index for elements.The disadvantage of array is that it needs continuous memory area and it is more difficult for expansion because all indexes need to be calculated once you insert/delete any element.
LinkedList is fast for element update because it only needs to break/point links between the certain nodes. It does not require a continuous memory area. However the disadvantage is that it is difficult for element search because linkedlist always needs to search from the first node

Q2: What are the oops concepts?

The main 3 OOPS concepts are:

  1. Abstraction

Abstraction means hidding the details of items and describing items in simple / common terms. For example we say there is a car rather than there is a red card.

  1. Ecapsulation

Ecapsulation means hidding the details of an object and explore interface which I want to explore to the outside.

  1. Polymorphism

Polymorphism means that for the same object, it can behave in different ways in different situations.

Q3: Can you talk about IOC and AOP?

IOC is short for inversion of control which means that the implementation is controlled by abstract level rather than details or certain items. In Spring framework IOC is implemented by Dependency Injection(DI). DI means we only needs to declare an abstract interface/class and the implementation will be injected in runtime. The porpuse of IOC/DI is decoupling.
AOP is short for Aspect Oriented Programming. It is an extension of OOP. The target of OOP is object while the target of AOP is aspect which means edges between objects/methods/modules.

Q4: You talked about Spring so how many types of AOP you know in Spring?

There’re 4 types of AOP in Spring:
Dynamic proxy - Spring uses JDK proxy for interfaces and CGLib for classes
Java Object AOP -
@Aspect annotation - TODO

Q5: How many types of advice you know in Spring?

我回答的比较抽象,我跟他说了一下我们自己做框架的时候做切口的思路
6 types of advice in Spring:
1.Before
2.After
3.Around
4.After Returning
5.After throwing
6.DeclareParents

Q6: What are Spring bean scopes?

  1. Singleton
  2. Prototype
  3. global session
  4. request

Q7: What’s the difference btw REQUIRED_NEW and nested transaction in Spring transaction?

REQUIRED_NEW means start a brand new transaction everytime.

Q8: What does Spring MVC do when it receives a request from client?

Request - > DIspatcherServlet - > handler - > model view / rest data - > client.

Q9: What’s the difference between Controller and RestController?

Controller will find a model view and render while restcontroller directly write result into response body.

Q10: What is the volatile keyword and how it works?

Volatile is a key word for fields.
It is a simple synchronization tool.
Volatile has 2 functions:

  1. prevent JVM from
  2. force threads to read content from direct memory.

Q11: What design patterns you prefer and why?

I prefer Pipeline cuz it is stupid but easy to understand.It can clearly define how the program works step by step. In Spring there’s pipelines such as request handling pipeline, exception handling pipeline, security chain, etc..
Besides I like command and stratege mode because they can help you avoid writting lots of stupid if/else blocks. There’re lots of use case of command/stratege in workflow engines such as Floable/Camunda.

Q12: You said command and stratege so what’s the difference?

Command is focus on the command itself that you main code is to construct a context of a command.
Stratege is not focus on the context. It focus on stratege itself which means the function logic.

Q13: How do you find / tract an issue in a project, for example, a project composed by hundres of miscroservices?

My method is..
1st step, find which area the issue located. The area means network/application/service.
2nd, for example, I’ve found the application/service

Q14: How do you organize the network structure of an microservice project?

Q15: What is the biggest challenge in your work?

Q16: How do you like microservice?What’s the difference for you?

it brings a better business / structure understanding to team members but also more difficult to maintain.

Q17: 你是怎么理解Restful的

请自行百度

Q18: HashMap和ConcurrentHashMap有什么不同?

java8中两者实现基本相同,ConcurrentHashMap在修改相关的方法上,使用了synchronized关键字,因此ConcurrentHashMap是线程安全的map.
在java8之前,ConcurrentHashMap内部是分段map的形式,在每一段上使用ReetrantLock以达到线程安全的目的。

Q19: 可以说以下HashMap的put方法都做了什么吗?

详见源码,幸好中文问的要不还说不清楚

  1. HashMap使用自己实现的Hash方法,计算得到Key的hash值
  2. 检查内部的Node节点数组(可称作哈希槽)是否为空,如果为空,调用resize方法进行初始化(resize也负责扩容)
  3. 扩容的逻辑:如果哈希槽为空,先创建一个默认初始长度为16的数组;如果哈希槽已经初始化,检查是否达到扩容阈值,默认为0.75*当前长度,如果达到了,扩容为当前2倍
  4. 接下来继续走put流程,通过(n-1)&hash 计算出元素该位于哈希槽的哪个位置,其中n为当前哈希槽长度,hash为HashMap计算出的key的hash值, 如果该位置上没有东西,直接创建个Node放在这里
  5. 如果发现此处已有Node,开始判断是否重复,如果此处的Node的hash和key值,和要put的key的hash值和本身的值均相同,那么就认为是重复元素,直接返回。
  6. 如果发现此处已有Node, 但是并非重复,如果是TreeNode, 即链表已转化为红黑树,此时往红黑树中插入元素
  7. 如果发现此处已有Node, 但是并非重复,如果不是TreeNode, 即此时还是链表,先将元素插入到链表尾,再调用treeifBin方法,判断如果哈希槽长度小于64,先尝试resize扩大哈希槽;如果哈希槽长度小于64,切链表长度超过8,将链表转化为红黑树。
  8. 最后再判断,如果当前Map容量超过75%, 即0.75*当前容量, 再resize一次

Q20: 什么是堆

堆是JVM内存模型中,线程间共享的内存区域,其在逻辑上连续,在物理上不连续。我们创建的对象本身就放在堆中。

Q21: String s1 = new String(“1”); String s2 = new String(“2”); s1和s2有什么区别?

“1”和”2”在堆中(1.8之前在方法区)的字符串常量池里;
new的2个String对象在堆中
s1,s2 两个引用是在栈当中,指向堆中的2个String对象.

Q22: 在生产环境上你是如何处理jvm内存问题的?

加参数,oom的时候打dump
分析dump,查看发生问题的地方的代码
调整gc

Q23: 说一说几种垃圾回收算法

8之前默认是CMS算法
8开始默认是g1算法
11开始是zgc
具体请自行百度

Q24: 如果一句SQL效率有问题,你是如何分析的?

首先第一步,看表建的有没有问题,例如高低频字段要分开,外键是否对效率产生了影响
第二步,查看sql的执行计划,找出慢的步骤
第三步,看有没有建立相关索引,或者出现索引失效的问题,并且修复
第四步,可以使用mysql或者oracle的工具,sql advisor自动优化sql.

Q25: 你和你的team member在design上出现分歧的时候,你是如何解决的?

PS: 可能有相关的管理方法论,但是我不知道,我是这么回的:
在我担任TL的一开始时,当我遇到这种情况时,首先我会召开讨论,向其它说说明我的思路,尝试说服他人,或者别人有更好的,可信的方式,我会接受意见并共同作出修改。当僵持不下时,我会引入第三方加入讨论,例如architect。后来我在流程上做了一点点优化,在出design之前,会有design idea的事先沟通,将流程打碎。

Q26: 你是如何对工作量进行评估的?

我对工作量的估算有2个前提,第一我们知晓常规职级的工程师在单位时间内能完成的工作量,称作1个point, point同时应当表征任务难度;第二,我自己应知晓我的组员在单位周期内能完成的point量,通过以上两点,我可以估算出任务量以及任务完成进度,之后再加上一些buffer, 得出组内大概的任务燃尽图。

Q27: 你知道T-shirt size估算方法吗?

我回的不知道,确实不知道。
后面百度了一下,大概跟估算工作的方法差不多,t-shirt有自己的size, 我有自己的body size, 做match.

Q28: 怎样创建一个SpringBoot Starter

Q29: 你用过哪些微服务的组件,它们是做什么的?

我只用过Spring Cloud/Netflix/Spring Cloud Alibaba就挨着说一遍.
服务发现与配置:Nacos / Eureka / ZooKeeper
网关:SpringCloud Gateway / Zuul
限流:Hystrix / Sentinel
客户端: OpenFeign
客户端负载均衡:Ribbon / Spring Load Balancer
分布式事务:seata

Q30: 一套全新的微服务系统,你如何规划它的网络?

Q31: 你刚才提到外部的负载均衡,在它外面还有什么?

Q32: 你在项目中是如何使用RedisCluster的?

Q33: MongoDB是AP还是CP?

我回答的不知道,真的不知道,实际上并没有Mongo的生产经验,没有关注
百度了一下,在默认情况下,Mongo是CP的。

Q34: 你在项目中是如何做CI/CD的?

Q35: 你在什么场景下使用Docker?

Q36: 我看你使用过RabbitMQ, 是在什么场景下使用的呢?它和Kafka的区别是什么?

RabbitMQ是轻量级的消息队列,一般用作需要异步处理的情况,例如削峰,执行次要任务等,但是一般简单的情况下我会使用Redis的list来实现简单消息队列。
Kafka是流式处理中间件,由于其自身设计,其性能远高于RabbitMQ, 可用做消息队列,但大多情况下用于做流式的数据处理。

Q37: Coding: Create three threads to add data into Array from a Queue in sequence.

3个线程轮流从queue中取数放入数组,无非是考:

  1. 多线程同步,达到轮流效果
  2. 线程安全的2个容器,一个queue一个数组

我用的CyclickBarrier,之前网上看到过,其它实现方法很多

从网上反馈来看,java开发会考一些简单算法如排序遍历和一些二分思想,用的一个在线编程平台写或者是选择题选代码片段
senior java一般是多线程题,但都不是很复杂
我是叫我直接共享屏幕打开摄像头看着我写的,写完后将代码复制给他。
我是想起之前看到的一个 CyclicBarrier ,就直接写了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
* TI-coding
* Author: Jarry Zhou
* Date: 2022/3/28
* Description: Create three threads to add data into Array from a Queue<Integer> in sequence.
**/
public class Test {
public static void main(String[] args) {
BlockingQueue<Integer> q = new LinkedBlockingQueue<>();
q.offer(1);
q.offer(2);
q.offer(3);
q.offer(4);
q.offer(5);
AtomicInteger index = new AtomicInteger(0);
CyclicBarrier b1 = new CyclicBarrier(2);
CyclicBarrier b2 = new CyclicBarrier(2);
CyclicBarrier b3 = new CyclicBarrier(3);

new Thread(() -> {
while (!q.isEmpty()) {
try {
Optional.ofNullable(q.poll()).ifPresent(val -> {
final int idx = index.getAndIncrement();
System.out.println(Thread.currentThread().getName() + " puts " + val + " into index " + idx);
});
b1.await();
b3.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();

new Thread(() -> {
while (!q.isEmpty()) {
try {
b1.await();
Optional.ofNullable(q.poll()).ifPresent(val -> {
final int idx = index.getAndIncrement();
System.out.println(Thread.currentThread().getName() + " puts " + val + " into index " + idx);
});
b2.await();
b3.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();

new Thread(() -> {
while (!q.isEmpty()) {
try {
b2.await();
Optional.ofNullable(q.poll()).ifPresent(val -> {
final int idx = index.getAndIncrement();
System.out.println(Thread.currentThread().getName() + " puts " + val + " into index " + idx);
});
b3.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();

}
}

AIA - Fullstack

面试时间: 60分钟
形式: 线上面试
面试结果:通过/拿到Offer
总结:问的比较随心所欲,一句java没问

1. docker file 里面的 run 和 cmd 的区别是什么

docker file是用于构建docker镜像的文本描述文件。
run命令用于执行后续语句,例如

1
RUN echo '这是一个本地构建的nginx镜像'

cmd命令类似于run指令
类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:
CMD 在docker run 时运行,RUN 是在 docker build时运行

2. 同时启动3个应用,使用docker compose,该怎么做

这个问题其实不是很好理解,docker compose一般就是用来做复杂或者多应用的。我的回答是在docker-compose.yml中依次定义应用即可,面试官不置可否。

2. 一个全新的大型应用,你如何规划网络

我回答的是vpc规划

3. https双向连接怎么做

3次握手建立tcp连接
客户端开始发起ssl连接,
1.发送客户端ssl版本和支持的加密算法类型
2.服务端返回ca证书,包含公钥,颁发机构,有效期
3.客户端使用自己的根证书验证服务端返回的公钥
4.客户端生成随机密码,并用公钥加密,发送给服务端
5.服务端使用私钥解密,得到对称密钥
6.双方开始加密通信
其中前几步可称做身份验证,验证对方是否是有效机构颁发的证书
后面几步就是密钥协商

4. 如何封装axios

这个比较一般一句话带过了了,实在是懒得说

5. jks大致命令

实际上我遇到的还很少有人说jks,就跟juc一样的,不知道什么时候开始莫名其妙开始说简称。。
keytool这玩意儿我用了这么多年,面试的时候居然忘记了,呵呵。

AQS简述

AQS即类AbstractQueuedSynchronizer,抽象队列同步器,指的是java构建锁和同步组件的基础框架。
JUC包中的 java.util.concurrent.locks.AbstractQueuedSynchronizer是一个抽象类,本身没有实现任何的同步接口,只是定义了同步状态,以及同步状态的获取和释放。
实际运用中通过继承它的方式来实现同步功能,一般是某种同步组件的静态内部类继承它,所以说aqs的实现类以组合的形式存在于同步组件,一同来实现同步功能。

AQS主要结构

AQS主要有以下的属性和方法,还有一个内部类Node

  1. 内部类Node, 它是一个双向链表节点,表征一个阻塞的线程

    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
    static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    static final int CANCELLED = 1;
    static final int SIGNAL = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
    final boolean isShared() {
    return nextWaiter == SHARED;
    }

    final Node predecessor() throws NullPointerException {
    Node p = prev;
    if (p == null)
    throw new NullPointerException();
    else
    return p;
    }

    Node() { // Used to establish initial head or SHARED marker
    }

    Node(Thread thread, Node mode) { // Used by addWaiter
    this.nextWaiter = mode;
    this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
    this.waitStatus = waitStatus;
    this.thread = thread;
    }
    }
  2. 一个Head一个tail, 就是一个队列的头尾,也就是说AQS内部有一个阻塞线程的队列

    1
    2
    private transient volatile Node head;
    private transient volatile Node tail;
  3. 一个volatile的state, 表示同步的状态,0为可用,1为被锁,以及它的get/set

    1
    2
    3
    4
    5
    6
    7
    private volatile int state;
    protected final int getState() {
    return state;
    }
    protected final void setState(int newState) {
    state = newState;
    }
  4. 一个state的cas方法

    1
    2
    3
    protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

AQS类本身就这么点东西,重要的是它的几个实现以及相关的同步组件。
查看AQS的子类,可以看到就如下几个
image.png

JUC常用的同步组件及应用场景

上文中可以看到,AQS主要用于各个同步组件中实现同步功能。
这里把常用的一些异步组件过一遍,看看它们中的AQS实现以及本身的应用场景。

  • ReentrantLock

ReentrantLock,可重入锁,里面的静态内部类Sync继承了AQS, Sync本身还是个抽象类,然后FairSync, NonfairSync又分别继承了Sync,分别实现了公平锁和非公平锁,因此ReentrantLock有公平锁和非公平锁两种实现,通过构造方法传入的boolean可以区分,默认是非公平锁,因为效率较高。
ReentrantLock可以用在用synchronized的场景,做线程同步。由于jdk优化后,synchronized一样可以cas, 甚至于java都建议多用synchronized了,但是它的优点在于它是一套api,所以比synchronized灵活,比如以下情况:

  1. 和synchronized一样用来同步

    1
    2
    3
    4
    5
    6
    7
    8
    // 最土的用法
    private ReentrantLock lock = new ReentrantLock(true); //公平锁,或者非公平锁
    try {
    lock.lock(); //如果被其它资源锁定,会在此等
    //操作
    } finally {
    lock.unlock();
    }
  2. 多线程竞争,发现有人获得锁开始执行了,其它的就不执行了,或者等一会儿再看看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // tryLock, 如果获得不了锁,整个就算了。
    ReentrantLock lock = new ReentrantLock();
    if (lock.tryLock()) { //尝试获取锁,或者用 lock.tryLock(5, TimeUnit.SECONDS),尝试不成功后再试一会儿
    try {
    //XXXXX操作
    } finally {
    lock.unlock();
    }

    }
  3. 可中断锁,获得锁开始操作,但是做一半可以中断的情况, 其它线程调用interrupt方法后,它抛出一个InterruptedException,就中断了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ReentrantLock lock = new ReentrantLock();
    try {
    lock.lockInterruptibly();// 获得一个可中断锁
    //xxxxx操作
    } catch (InterruptedException e) {
    e.printStackTrace();
    } finally {
    lock.unlock();
    }
  • ReentrantReadWriteLock

和ReentrantLock一模一样,AQS在其中有公平锁和非公平锁两个实现。
ReentrantReadWriteLock和ReentrantLock的区别在于,ReentrantReadWriteLock里面维护了2个锁,一个read读锁,一个write写锁,为的是针对大多数情况读而少数写的情况,这种情况下读用的读锁是共享锁,写用的写锁是排他锁,而ReentrantLock只有一个排他锁。

  • Semaphore

同上,AQS在其中实现公平锁和非公平锁
Semaphore 信号量,一般来说锁只允许一个线程获得,而信号量允许多个线程同时获得资源,通过构造函数可以制定同时允许的线程数,因此它用于限制同时间能获取资源的线程数。
为了做到这点,Semaphore里的AQS的实现类Sync中的state,维护的是信号量个数
比如最常见的例子,银行有5个柜台能办理业务,但是有100个人等待办理,这100个人要是排队,就是公平锁,靠本事插队,就是非公平锁

1
2
3
4
5
6
7
8
9
10
11
12
13
Semaphore semaphore = new Semaphore(5, true); // 最多5个,公平锁
try {
semaphore.acquire(1);//获取一个许可
//do sth
return par1+"获取一个许可";
}catch (InterruptedException e){
e.printStackTrace();
return "获取许可失败";
}catch (Exception e1){
return "获取许可失败";
}finally {
semaphore.release(1);//处理完后,释放一个许可
}
  • CountDownLatch

CountDownLatch,从字面意思上看是计时门闩,门闩的地方负责阻塞线程,所以它用于需要一组线程等待彼此都到达门闩处的情况。
CountDownLatch只有一个构造方法,传入一个int, 也就是AQS里的state, 它表征拦截的线程数量,即AQS中阻塞队列的长度,同时也是倒计时的数字。
CountDownLatch的主要方法有
await() - 阻塞当前线程,计数至0之前一直等待,除被中断
countDown() - 计数器-1
曾经看到群里有人遇到的面试题,如何将异步处理的结果做聚合,例如多个线程去不同的地方取数据,然后做统一运算返回,这种情况就可以用CountDownLatch。

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
CountDownLatch latch = new CountDownLatch(5); // 拦5个线程
for (int i=0; i<5; i++) { // 创建5个线程开始跑,每个睡5秒之后打印一句
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + " 运行完了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 跑完之后计数 -1, 当5个线程都跑完了,计数器就归0了
}
}
}).start();
}

latch.await(10, TimeUnit.SECONDS); // 把当前线程阻塞了,等其它线程跑完
System.out.println("主线程开始做事了!");
/*
输出结果
Thread-1 运行完了
Thread-5 运行完了
Thread-2 运行完了
Thread-3 运行完了
Thread-4 运行完了
主线程开始做事了!

可见主线程是等着其它线程都跑完了之后才开始干事的, 如果把代码中的CountDownLatch部分注释掉,就是这种结果了
主线程开始做事了!
Thread-3 运行完了
Thread-2 运行完了
Thread-4 运行完了
Thread-1 运行完了
Thread-5 运行完了
*/

  • CyclicBarrier

字面意思,可重复使用的栅栏。功能可以理解为可以一堆CountDownLatch连接起来,CountDownLatch只拦截一次,然后放行,CyclicBarrier可以循环地阻塞拦截。
可以说CyclicBarrier比CountDownLatch更强大。
不同的是,CyclicBarrier中的线程没有区分等人的和被等的,大家都是互相等,另外,不需要显示地去countDown.

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
CyclicBarrier b = new CyclicBarrier(5); // 拦5个线程
for (int i=0; i<5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 到达栅栏1开始等待");
b.await();
System.out.println(Thread.currentThread().getName() + " 完成工作1,到达栅栏2开始等待");
b.await();
System.out.println(Thread.currentThread().getName() + " 开始工作2");
} catch (Exception e) {
e.printStackTrace();
} finally {

}
}
}).start();
}

/*
结果:
Thread-1 到达栅栏1开始等待
Thread-3 到达栅栏1开始等待
Thread-2 到达栅栏1开始等待
Thread-4 到达栅栏1开始等待
Thread-5 到达栅栏1开始等待
Thread-5 完成工作1,到达栅栏2开始等待
Thread-1 完成工作1,到达栅栏2开始等待
Thread-2 完成工作1,到达栅栏2开始等待
Thread-3 完成工作1,到达栅栏2开始等待
Thread-4 完成工作1,到达栅栏2开始等待
Thread-4 开始工作2
Thread-5 开始工作2
Thread-1 开始工作2
Thread-3 开始工作2
Thread-2 开始工作2
*/

综述

AQS抽象队列同步器,定义了同步状态,阻塞线程队列,以及同步状态设置方法。
JUC包里的同步组件们组合各种AQS的实现类,实现同步的功能。
同步组件功能比synchronized关键字更加丰富和灵活,不同的同步组件可以应对不同的使用场景。

利用Java-parser进行Java抽象语法树分析

JavaParser

https://github.com/javaparser/javaparser
JavaPaser是Github上的一个开源项目,提供了简单清晰的API,对Java1.0至Java15进行抽象语法树分析。
在有了AST之后,便可以基于此进行语法检查,代码风格检查,格式化,高亮,自动补全,错误提示,代码混淆,压缩,代码变更等等操作。

JavaParser提供了一系列类、API来表达Java抽象语法树:

  • 类 com.github.javaparser.JavaParser - 提供来一套API从源码生成AST
  • 类 com.github.javaparser.StaticJavaParser - 提供静态方法快速从源码生成AST
  • 类 com.github.javaparser.ast.CompilationUnit - 语法树的节点单元,每个java文件都会产出一个CompilationUnit。CompilationUnit中从一个可选的包声明开始(package),接下来是导入声明(import),然后是类型声明(属性)。

接下来看一个简单代码片段,从一个source jar中生成AST,并找出所有方法抛出的异常

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
public static void main(String[] args) {
String sourceJarPath = "x x x x x";
final ParserConfiguration parserConfiguration = new ParserConfiguration();
parserConfiguration.setDetectOriginalLineSeparator(true);
parserConfiguration.setLexicalPreservationEnabled(true);
parserConfiguration.setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_8);
final JavaParser javaParser = new JavaParser(parserConfiguration);
try {
final ParseResult<CompilationUnit> result = javaParser.parse(Paths.get(sourceJarPath).normalize());
if (result == null ||! result.isSuccessful() ||! result.getResult().isPresent()) {
return;
}
final CompilationUnit compilationUnit = result.getResult().get();
LexicalPreservingPrinter.setup(compilationUnit);

compilationUnit.findAll(ClassOrInterfaceDeclaration.class).parallelStream()
.peek(clz->{System.out.println("类"+clz.getFullyQualifiedName().orElse(null));})
.flatMap(clz->clz.getMethods().stream())
.filter(m->m.getThrownExceptions()!=null&&!m.getThrownExceptions().isEmpty())
.forEach(method->{
System.out.println("方法: "+method.getDeclarationAsString());
System.out.println("抛出异常: "+method.getThrownExceptions().stream().map(ReferenceType::toDescriptor).collect(Collectors.joining(", ")));
});
} catch (Exception e) {
e.printStackTrace();
}
}

请我喝杯咖啡吧

微信