倾诉心声,沟通之道:人际交往中的表达技巧

本文最后更新于:1 年前

真正的幸福,不是拥有多少财富和地位,而是内心的充实与满足,以及身边人的陪伴与关爱。

放在前面

昨天跟刚认识的同学聊了一个多小时,感触很深,我能够把开心带给身边的人,我也感到很开心。我的思维很活跃,有很多想要表达的东西,但我平时是不太喜欢讲话的,在一伙人中间,我肯定是最安静的那个。

但这并不代表着我不敢讲、不喜欢讲话,我只是不讲话而已,恰恰相反,只要在我感兴趣的领域,我还是很喜欢跟别人聊天的:

周一上午跟导员作检讨,为什么要无故旷课;周三中午跟舍友聊专业规划、学习经验,扯了一中午;周五下午跟刚认识的女同学聊期末复习备考经验、未来规划,分享自己的烦恼、糗事。

人生在世,人际交往是很重要的一环。很喜欢《被讨厌的勇气》里提到的一句话:一切烦恼的根源都来自于人际关系,非常推荐大家看看这本书。

别人听我讲完话,能学习到很多,更加清晰的规划,更加乐观地面对生活等等,这就让我感到很满足了,我能够输出一些东西,我的输出是有价值的。

一个月前开设的新栏目:《个人成长之路:计划演进与思想蜕变的见证》,如果说这个栏目是记录我的内心所想的话,那么今天开设的新栏目:《倾诉心声,沟通之道:人际交往中的表达技巧》,就是关于如何把内心所想表达出来,相信我,这真的非常重要。

先说这么多吧,这个栏目涉及的东西还挺杂,随便写写。

思想火花

八股文吟唱技巧

  • 这里提出我的表述技巧:“口语表达” 和 “定义后置
  • 口语化表达,显而易见,就是不要像背课文一样,问什么答什么,一句多余的扩展都没有,比如:

问:对事务隔离级别有了解吗?(2023/11/24 晚)

答:哦,事务的隔离级别有四种:读未提交、读已提交、可重复读、串行化,然后巴拉巴拉稍微说下这四种隔离级别是用来处理事务的,,,

这里给出 AI 的回答范例

1
2
3
4
5
6
7
8
9
10
11
哦,事务的隔离级别啊?就是那个...呃...你懂的,就是数据库里面处理事务的时候用的一种方式嘛。

首先,有读未提交(Read Uncommitted),这个隔离级别很低级,因为它允许一个事务读取另一个事务还没提交的数据。这样可能会出现数据不一致的问题,但是可以提高性能。

然后是读已提交(Read Committed),这个隔离级别比读未提交高一点,因为只有当一个事务提交了之后,其他事务才能看到它修改的数据。这样就避免了脏读(Dirty Read)的问题,但可能会导致不可重复读(Non-repeatable Read)。

接下来是可重复读(Repeatable Read),这个隔离级别又高了一点,它确保在一个事务内多次读取同一行数据时,结果是一致的。这样就解决了不可重复读的问题,但可能会导致幻读(Phantom Read)。

最后是串行化(Serializable),这个隔离级别最高,它要求事务串行执行,避免了所有的并发问题,但性能最差。

总的来说,不同的隔离级别有不同的优缺点,要根据具体的场景来选择。不过说实话,我觉得这些东西好复杂,我一般都是用默认的设置就好了。
  • 基本符合
  • 但如果你按照这样的思路回答呢?
  • 介绍下事务是什么(一组不可分割的操作,一个事务中的所有操作,要么全部执行成功,要么全部执行失败),事务的特性(原子性、一致性、隔离性、持久性)。然后重点介绍事务的隔离性:为什么事务要有隔离性,这就要了解并发事务访问共享数据时可能会引发的问题:脏读、不可重复读和幻读。所以 SQL 标准提出了四种事务的隔离级别来解决这些问题。不同的数据库厂商对 SQL 标准中规定的 4 种隔离级别的支持不一样,MySQL 就支持全部四种隔离级别
  • 简单解释这四种事务隔离级别的具体实现方式:
  • 读未提交:直接读取最新数据,串行化:通过添加读写锁,读已提交、可重复读:通过 Read View 实现(MVCC)
  • 这样就建立了一个对数据库事务的全面的认识了
  • 这样不仅需要熟练掌握相关知识,还需要用口语化的语言阐述

说一下多态的实现原理?

  • 问题:说一下多态的实现原理?

  • 你背过的八股:

    什么是多态?

    多态是面向对象编程中的一个重要概念,它允许通过父类类型的引用变量来引用子类对象,并在运行时根据实际对象的类型来确定调用哪个方法。换句话说,一个对象可以根据不同的情况表现出多种形态。

    通过多态,我们可以利用父类类型的引用变量来指向子类对象,并根据实际对象的类型调用对应的方法。这样可以在不修改现有代码的情况下,动态地切换和扩展对象的行为。

    多态有以下几个特点和优势:

    1. 可替换性:子类对象可以随时替代父类对象,向上转型。
    2. 可扩展性:通过添加新的子类,可以扩展系统的功能。
    3. 接口统一性:可以通过父类类型的引用访问子类对象的方法,统一对象的接口。
    4. 代码的灵活性和可维护性:通过多态,可以将代码编写成通用的、松耦合的形式,提高代码的可维护性。
    多态的实现原理

    动态绑定和虚拟方法调用。

    动态绑定(Dynamic Binding):指的是在编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。这意味着,编译器会推迟方法的绑定(即方法的具体调用)到运行时。正是这种动态绑定机制,使得多态成为可能。

    虚拟方法调用(Virtual Method Invocation):在 Java 中,所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。当通过父类类型的引用变量调用被子类重写的方法时,虚拟机会根据实际对象的类型来确定要调用的方法版本,而不是根据引用变量的声明类型

    多态的具体实现

    方法重载、方法重写

  • 如何表达?核心在于你要把任何问题理解为:谈一谈你对多态的理解

    我们经常讲,面向对象有三大特征:封装、继承和多态

    多态是面向对象编程中相当重要的一个概念,它允许通过父类类型的引用变量引用子类对象,在运行时根据实际的对象类型来确定调用哪个方法,一个对象能够根据不同的场景表现出多种形态

    那多态具体是怎么实现的?在编译时,Java 编译器只能知道变量的声明类型,也就是父类类型,而无法确定实际的对象类型;而在运行时,Java 虚拟机会通过动态绑定解析出实际对象的类型,根据实际的对象类型调用被子类重写的方法。

    也就是说,编译器会把方法的绑定,即方法的具体调用推迟到运行时,这就是动态绑定,这就是多态的实现原理

    我们发现使用多态有这样的好处:我们通过父类类型的引用来访问子类对象的方法,统一对象的接口,这是接口统一性;子类对象可以随时替代父类对象,向上转型,这就是可替换性;我们可以通过添加新的子类,扩展系统功能,这就是可扩展性;通过多态,能够实现对象间的解耦,因为我们不再需要指定具体对象去实现具体方法了,这使得代码更加简洁通用、更加易于维护

    我们在编码开发中接触到的方法重载、方法重写、接口实现就是多态的具体实现,方法重载体现的是编译时的多态,而方法重写和接口实现体现的是运行时的多态

Java 中 String 内容不可变

我们经常听到这样的定义:String 对象一旦被创建,其内容就一定不可变

这句话的意思是:对该对象的所有操作(如 replace()contact()substring())都将返回新的 String 对象,而不是在原 String 对象的内容上作修改。这些操作也都是不被允许的:

1
2
3
4
5
6
7
8
9
String s = "Hello";

s.charAt(0) = 'h'; // 编译错误:Cannot assign to 'char' field in an array of 'final' value

s.length() = 5; // 编译错误:Cannot assign to 'int' field in an array of 'final' value

String t = s.substring(0, 5); // 可以,但结果是新的 String 对象,不是对原有对象的修改

t = t + " World"; // 可以,但结果是新的 String 对象,不是对原有对象的修改

为什么是这样的?String 对象内容不可变是如何保证的,这样做又有什么好处呢?

String 类被设计出来是为了方便我们对字符串进行操作,我们常见的字符串拼接、比较字符串内容、字符串长度等等,应用十分广泛。而 String 类底层是通过 char [] 数组(Java 9 之后改为 byte [] 实现了)来维护字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {

/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;

.................
}

需要注意的是,这个字符串使用了 final 关键字修饰。

我们都知道被 final 关键字修饰的类不能被继承、修饰的方法不能被重写、修饰的基本数据类型变量的值不能被改变,修饰的引用类型变量不能再指向其他对象。很显然,这里的 char [] 数组属于引用型变量,所以其内容是可以改变的

这就是很多人疑惑的点了:难道不是很奇怪吗?String 的内容是不可变的,但 String 底层是 final 修饰的 char[] 数组实现的,而这个数组内容是可变的。所以你给解释一下 String 内容不可变到底是怎么一回事

很多八股在这里都在扯淡,在这里我给出正确答案:这里的 char [] 数组属于引用型变量,理论上它的内容当然是可以改变的:

1
2
3
final String[] arr = new String[]{"Hello", "World"};

arr[0] = "Hi"; // 这里是可以的,因为arr[0]指向的对象(即"Hello")是可以改变的

但是这一点跟 String 内容是不可变的本身没有冲突,因为 String 并没有对外提供任何方法,去改变内置的 char [] 数组的内容,所以 String 对外表现出的 String 内容不可变,这就是:String 对象一旦被创建,其内容就一定不可变的正确解释

综上所述,String 类是不可变的,这意味着一旦一个 String 对象被创建,它的内容就不能被修改。即使 String 底层是通过 final 修饰的 char 数组实现的,但是这个 char 数组的内容也不能被修改,因为 String 并没有对外提供任何方法,允许我们去改变内置的 char [] 数组的内容。因此,即使我们可以访问到 String 对象的底层 char 数组,我们也不能通过改变这个数组来修改 String 对象的内容。任何尝试修改 String 对象内容的操作都会返回一个新的 String 对象,而原来的 String 对象保持不变

查看源码你就能清楚地看到这个过程了,当然源码很复杂,这里展示出 replace 的部分源码,你可以看到在执行这个操作的过程中,是 new 了新的 byte [] 的:

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 static String replace(byte[] value, char oldChar, char newChar) {
if (canEncode(oldChar)) {
int len = value.length;
int i = -1;
while (++i < len) {
if (value[i] == (byte)oldChar) {
break;
}
}
if (i < len) {
if (canEncode(newChar)) {
byte buf[] = new byte[len];
for (int j = 0; j < i; j++) { // TBD arraycopy?
buf[j] = value[j];
}
while (i < len) {
byte c = value[i];
buf[i] = (c == (byte)oldChar) ? (byte)newChar : c;
i++;
}
return new String(buf, LATIN1);
} else {
byte[] buf = StringUTF16.newBytesFor(len);
// inflate from latin1 to UTF16
inflate(value, 0, buf, 0, i);
while (i < len) {
char c = (char)(value[i] & 0xff);
StringUTF16.putChar(buf, i, (c == oldChar) ? newChar : c);
i++;
}
return new String(buf, UTF16);
}
}
}
return null; // for string to return this;
}

这样做有什么好处?这种不可变性是 Java String 类的一个重要特性,使得 String 可以安全地被共享和传递,而不需要担心其他部分的代码会修改它的内容

你能讲一下什么是 Maven 嘛?

Maven 是什么,Maven 是一个项目构建和管理工具,是 Apache 下的一个纯 Java 开发的开源项目,我们现在基本都在使用 Maven 来构建和管理 Java 项目,当然也有其他类似的的项目构建和管理工具,比如 Gradle。(2023/12/01 晚)

那么我们为什么要使用 Maven 呢?使用它有什么好处?我们首先要认识到,在没有使用 Maven 工具之前,项目构建和管理存在很多问题:

  • 依赖关系管理困难:在手动构建项目时,需要手动下载和添加项目所需的依赖库,这不仅耗时而且容易出错。此外,如果项目中有多个模块,需要确保每个模块都有正确的依赖版本,这需要花费大量时间和精力。
  • 构建过程繁琐:在没有自动化构建工具的情况下,开发人员需要手动编译、测试和打包项目。这不仅耗时,而且容易出错。此外,如果项目中有多个模块,需要分别构建每个模块,这会进一步增加构建的复杂性。
  • 项目结构五花八门:在没有统一的项目构建和管理规范的情况下,每个项目可能会有自己独特的项目结构,这使得项目之间的协作和交流变得困难。
  • 版本控制和发布困难:在没有 Maven 之前,版本控制和发布需要手动完成,这不仅耗时而且容易出错。此外,如果项目中有多个模块,需要分别发布每个模块,这会进一步增加发布的复杂性。
  • 团队协作效率低下:在没有 Maven 之前,团队成员之间需要手动共享项目文件和依赖库,这不仅效率低下,而且容易出错。此外,如果项目中有多个模块,需要分别管理每个模块的代码和依赖库,这会进一步降低团队协作的效率

总之,没有 Maven 之前,项目构建和管理可能会面临许多痛点,包括依赖关系管理困难构建过程繁琐项目结构五花八门版本控制和发布困难以及团队协作效率低下等问题。而 Maven 等自动化构建工具的出现有效地解决了这些问题,提高了项目构建和管理的效率和准确性。

那么 Maven 项目的结构是怎样的呢?通常包括以下几个部分:

  • src:包含了项目所有的源代码和资源文件以及测试代码。其中 src/main/java 这个目录下储存 java 源代码,src/main/resources 储存主要的资源文件,比如 spring 的 xml 配置文件和 log4j 的 properties 文件,src/test/java 存放测试代码。
  • target:编译后内容放置的文件夹。
  • pom.xml:这是 Maven 的基础配置文件,也是 Maven 项目核心配置文件,相当关键,用于配置项目的基本信息、依赖范围管理、解决依赖冲突,实现分模块开发、多环境配置

其他相关的还有:Maven 的私服搭建、配置 Maven 镜像源、Maven 的生命周期

1
Maven的生命周期是对所有的构建过程进行抽象和统一,它包含了项目的清理(clean)、初始化(validate)、编译(compile)、测试(test)、打包(package)、安装(install)、验证(verify)、部署(deploy)和站点生成(site)等几乎所有的构建步骤。在Maven中有三套相互独立的生命周期,分别是Clean Lifecycle、Default Lifecycle和Site Lifecycle。
1
2
3
4
5
6
7
install和package都是Maven生命周期中的阶段,但它们执行的操作和目的有所不同。

install阶段主要是将项目打包为jar文件,并将该文件安装到本地仓库中,以便其他项目可以重复使用该项目的依赖关系。install阶段通常在所有测试运行完毕后执行,它不仅打包了可执行的jar文件,还打包了测试jar文件和源码jar文件。

而package阶段主要是将项目打包为jar文件,该文件包含了项目的可执行代码和相关资源,并将其放置在项目的target目录下。这个阶段通常在compile阶段之后执行,它会生成一个可执行的jar包,可以在本地运行或者部署到其他环境中。

总的来说,install和package都涉及到项目的打包,但install是将项目打包到本地仓库中,以供其他项目使用;而package是将项目打包成可执行的jar包,以便在本地或部署到其他环境中运行。

说出你知道的所有 Java 里面保证线程安全的方式

Java 中保证线程安全的方式包括:(2023/12/01 晚)

  • synchronized 关键字:通过 synchronized 关键字可以实现对代码块或方法的同步,保证同一时刻只有一个线程执行该代码块或方法,从而避免多线程并发访问造成的数据不一致性。
  • ReentrantLock:ReentrantLock 是显示锁,通过 lock()和 unlock()方法来实现对临界区的加锁和解锁,提供了比 synchronized 更灵活的锁操作。
  • Atomic 类:java.util.concurrent.atomic 包下的 Atomic 类(如 AtomicInteger、AtomicLong 等)提供了原子操作,保证了对变量的操作是原子性的,从而避免了多线程并发访问带来的数据竞争问题。
  • 使用线程安全的集合类:如 ConcurrentHashMap、CopyOnWriteArrayList 等,这些集合类内部实现了线程安全机制,可以在多线程环境下安全地进行操作。
  • 使用 ThreadLocal 变量:ThreadLocal 可以实现每个线程拥有自己独立的变量副本,从而避免多线程之间的共享变量带来的线程安全问题。

谈谈我对微服务架构的理解

从单体应用迁移到微服务架构:

单体应用线上发布和部署效率低下、团队协作开发成本高、系统可用性差。

相较于单体应用,微服务实现了更细粒度的服务拆分、更高效的服务部署和独立维护、提供了更清晰的服务治理方案。

要实现从单体应用迁移到微服务架构,就要做好服务化拆分,设计服务间调用。明确微服务架构的基本组件:服务描述、注册中心、服务框架、服务监控、服务追踪、服务治理。

服务发布和引用

注册中心(服务注册和发现)

在微服务架构下,主要有三种角色:服务提供者(RPC Server)、服务消费者(RPC Client)和服务注册中心(Registry),三者的交互关系请看下面这张图,我来简单解释一下。

RPC Server 提供服务,在启动时,根据服务发布文件 server.xml 中的配置的信息,向 Registry 注册自身服务,并向 Registry 定期发送心跳汇报存活状态。

RPC Client 调用服务,在启动时,根据服务引用文件 client.xml 中配置的信息,向 Registry 订阅服务,把 Registry 返回的服务节点列表缓存在本地内存中,并与 RPC Sever 建立连接。

当 RPC Server 节点发生变更时,Registry 会同步变更,RPC Client 感知后会刷新本地内存中缓存的服务节点列表。

RPC Client 从本地缓存的服务节点列表中,基于负载均衡算法选择一台 RPC Sever 发起调用。

RPC 调用

想要完成 RPC 调用,你需要解决四个问题:

  • 客户端和服务端如何建立网络连接?
  • 服务端如何处理请求?(选择合适的通信框架比如 Netty,解决客户端与服务端如何建立连接、管理连接以及服务端如何处理请求的问题)
  • 数据传输采用什么协议?(选择合适的通信协议比如 HTTP,解决客户端和服务端采用哪种数据传输协议的问题)
  • 数据该如何序列化和反序列化?(多种序列化格式比如 Java 原生序列化、JSON、XML、Thrift,解决客户端和服务端采用哪种数据编解码的问题)

Socket 通信

Socket 通信是基于 TCP/IP 协议的封装,建立一次 Socket 连接至少需要一对套接字,其中一个运行于客户端,称为 ClientSocket ;另一个运行于服务器端,称为 ServerSocket 。就像下图所描述的,Socket 通信的过程分为四个步骤:服务器监听、客户端请求、连接确认、数据传输。

  • 服务器监听:ServerSocket 通过调用 bind()函数绑定某个具体端口,然后调用 listen()函数实时监控网络状态,等待客户端的连接请求。
  • 客户端请求:ClientSocket 调用 connect()函数向 ServerSocket 绑定的地址和端口发起连接请求。
  • 服务端连接确认:当 ServerSocket 监听到或者接收到 ClientSocket 的连接请求时,调用 accept()函数响应 ClientSocket 的请求,同客户端建立连接。
  • 数据传输:当 ClientSocket 和 ServerSocket 建立连接后,ClientSocket 调用 send()函数,ServerSocket 调用 receive()函数,ServerSocket 处理完请求后,调用 send()函数,ClientSocket 调用 receive()函数,就可以得到得到返回结果。

服务监控

服务监控主要包括四个流程:数据采集、数据传输、数据处理和数据展示

监控对象有用户端监控、接口监控、资源监控、基础监控,监控指标有请求量、响应时间和错误率

服务追踪

在微服务架构下,由于进行了服务拆分,一次请求往往需要涉及多个服务,每个服务可能是由不同的团队开发,使用了不同的编程语言,还有可能部署在不同的机器上,分布在不同的数据中心。

如果有一个系统,可以跟踪记录一次用户请求都发起了哪些调用,经过哪些服务处理,并且记录每一次调用所涉及的服务的详细信息,这时候如果发生调用失败,你就可以通过这个日志快速定位是在哪个环节出了问题,这个系统就是今天我要讲解的服务追踪系统。

服务治理

在一次服务调用过程中,服务提供者、服务消费者、注册中心、网络都有可能出问题。我们要尽可能保证服务调用成功,这就是服务治理。

服务治理可以从很多方面考虑:

  • 从服务健康状态考虑。要做到服务节点管理,比如使用心跳检测机制,这种机制要求服务提供者定时的主动向注册中心汇报心跳
  • 从服务节点访问优先级考虑。一般情况下,服务提供者节点不是唯一的,多是以集群的方式存在。要选择合适的负载均衡策略,充分利用机器的性能。
  • 从调用的健康状态考虑。服务调用并不总是一定成功的,对于服务调用失败的情况,需要有手段自动恢复,来保证调用成功。

点睛之笔


倾诉心声,沟通之道:人际交往中的表达技巧
https://test.atomgit.net/blog/2023/11/18/倾诉心声,沟通之道:人际交往中的表达技巧/
作者
Memory
发布于
2023年11月18日
更新于
2023年5月13日
许可协议