exist和in有什么区别
select * from A where id in(select id from B)
有两点区别:
(1) 使用上的区别:exists中放一个子查询有记录返回true,无记录返回false(NULL也算有记录),in中查询结果集只能有一个字段
(2) 性能上的区别:in要把缓存到内存中,exists不需要缓存结果
in()适合B表比A表数据小的情况
exists()适合B表比A表数据大的情况
当A表数据与B表数据一样大时,in与exists效率差不多,可任选一个使用.
union和union all的有什么不同
UNION
和 UNION ALL
是用于合并两个或多个 SELECT 语句的结果集的 SQL 操作符,它们之间有一些重要的区别:
-
UNION:
UNION
用于合并两个或多个 SELECT 语句的结果集,并且会自动去除重复的行。- 如果在
UNION
操作中有重复的行,只会返回一次。 UNION
执行过程中会对结果集进行排序和去重,因此相比UNION ALL
,它可能会更消耗资源,性能可能会稍低。
-
UNION ALL:
UNION ALL
同样用于合并两个或多个 SELECT 语句的结果集,但不会去除重复的行。- 如果在
UNION ALL
操作中有重复的行,会返回多次。 UNION ALL
不会对结果集进行排序和去重,因此相比UNION
,它的性能可能更高。
主要区别:
UNION
去除重复行,而UNION ALL
不去除重复行。UNION
需要对结果集进行排序和去重,而UNION ALL
不需要。
在选择使用 UNION
还是 UNION ALL
时,需要根据实际需求来决定是否需要去除重复行,以及对性能的要求。如果确实需要去除重复行,并且对性能影响可以接受,可以选择使用 UNION
,否则可以考虑使用 UNION ALL
。
子事务
jdbcTemplate
自定义start启动器
在Java中自定义启动器通常涉及到创建自定义的启动类,该启动类将负责初始化和启动应用程序。以下是一种常见的方式来创建自定义的启动器:
- 创建启动类: 首先,您需要创建一个包含main方法的Java类,该类将作为应用程序的入口点。这个类可以执行一些初始化操作,然后启动应用程序的主要逻辑。
public class MyAppLauncher {
public static void main(String[] args) {
// 执行应用程序初始化操作
initialize();
// 启动应用程序主逻辑
startApplication();
}
private static void initialize() {
// 进行应用程序初始化,例如加载配置、初始化数据库连接等
}
private static void startApplication() {
// 启动应用程序的主逻辑,例如启动服务器、加载UI界面等
}
}
-
构建启动器: 接下来,您可以将这个启动类打包成一个可执行的JAR文件。您可以使用Maven或Gradle等构建工具来构建和管理项目。
-
运行应用程序: 最后,您可以通过命令行或其他方式来运行这个自定义启动器的JAR文件。例如:
java -jar MyAppLauncher.jar
或者您也可以在IDE中直接运行启动类的main方法。
通过这种方式,您可以实现一个自定义的Java应用程序启动器,该启动器可以执行一些初始化操作并启动应用程序的主逻辑。您可以根据实际需求来扩展和定制启动器的功能。
自定义注解
在Java中,您可以使用自定义注解来为类、方法、字段等元素添加元数据信息,以及定义和使用特定的标记。以下是创建和使用自定义注解的一般步骤:
- 定义注解: 首先,您需要定义一个注解接口,使用
@interface
关键字来声明。注解接口中可以定义方法,这些方法可以被用来指定注解的属性。
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 指定注解保留策略
@Target(ElementType.METHOD) // 指定注解可以应用的目标元素类型
public @interface MyAnnotation {
String value() default ""; // 定义一个属性,默认值为空字符串
}
- 使用注解: 在需要的地方使用自定义注解,可以在类、方法、字段等上面使用。
public class MyClass {
@MyAnnotation("SomeValue") // 使用自定义注解,并指定属性值
public void myMethod() {
// 方法实现
}
}
- 读取注解信息: 您可以使用Java反射机制来读取注解信息。通过反射,您可以获取到注解的属性值等信息。
import java.lang.reflect.Method;
public class AnnotationProcessor {
public void processAnnotations(Class<?> clazz) {
Method[] methods = clazz.getDeclaredMethods(); // 获取类中声明的所有方法
for (Method method : methods) {
if (method.isAnnotationPresent(MyAnnotation.class)) { // 判断方法是否使用了指定的注解
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class); // 获取注解实例
String value = annotation.value(); // 获取注解属性值
System.out.println("Value from annotation: " + value);
}
}
}
}
这样,您就可以定义、使用和读取自定义注解了。自定义注解可以用于各种场景,例如在测试框架中标记测试方法、在AOP中标记切点等。通过注解,您可以为代码添加更多的语义信息和标记,从而提高代码的可读性和可维护性。
线程池的原理
线程池是一种用于处理并发任务的软件设计模式,它的核心思想是利用池化技术来实现资源的高效复用,从而避免频繁地创建和销毁线程所带来的性能开销。在Java中,通常是通过ThreadPoolExecutor
类来创建线程池。线程池的主要功能包括:
提升性能:线程池能够独立负责线程的创建和管理,允许将任务提交给线程池并让其调度执行,这样可以最大化地使用空闲线程来执行异步任务,从而显著提升性能。1
线程管理:线程池会维护一些基本线程统计信息,以便有效地管理和调度接收到的异步任务。
统一管理:线程池可以对线程进行统一的分配、调优和监控,提高了线程的可管理性。2
减少资源消耗:通过复用已存在的线程,线程池可以减少线程创建和销毁造成的资源消耗。
提高响应速度:当有任务到达时,线程池可以直接从池中获取线程来执行,而无需等待新线程的创建,这样可以快速响应用户请求。
线程池的工作流程通常是这样的:任务提交者将任务提交给线程池,线程池会根据配置决定是否创建一个新的线程来执行这个任务。如果线程池中有足够的空闲线程,它会使用这些线程来执行任务;如果没有足够线程,则会创建新的线程。一旦任务完成,线程会被放回线程池中,而不是被销毁,以便将来可以被再次使用。这种策略可以有效防止线程的过度创建和销毁,从而减少资源浪费和提高系统性能
线程池的拒绝策略
线程池的拒绝策略是指当线程池无法接受新任务时,如何处理这些新任务的一种策略。Java中的线程池框架 ThreadPoolExecutor
提供了几种内置的拒绝策略,以便开发人员根据实际情况选择适合的策略。下面是几种常见的拒绝策略:
-
AbortPolicy(默认策略):
- 这是默认的拒绝策略,当线程池饱和时,新任务将会被立即抛出
RejectedExecutionException
异常,阻止任务的提交。 - 这种策略可能会导致任务丢失,但能够尽快地发现并处理线程池无法处理任务的情况。
- 这是默认的拒绝策略,当线程池饱和时,新任务将会被立即抛出
-
CallerRunsPolicy:
- 当线程池饱和时,新任务会被调用者所在的线程执行,而不会抛出异常。
- 这种策略可以保证任务不会被丢失,但是会增加调用者的负担,因为调用者自己的线程会执行任务。
-
DiscardPolicy:
- 当线程池饱和时,新任务将被丢弃,不做任何处理。
- 这种策略会导致新任务被丢弃,不会有任何反馈或处理。
-
DiscardOldestPolicy:
- 当线程池饱和时,将丢弃队列中等待时间最长的任务,然后尝试重新提交新任务。
- 这种策略尝试丢弃一些等待时间最长的任务,从而为新任务腾出位置,但可能导致一些长时间等待的任务被丢弃。
-
自定义拒绝策略:
- 开发人员也可以通过实现
RejectedExecutionHandler
接口来定义自己的拒绝策略,根据实际需求进行处理。通过实现该接口,可以定义拒绝策略的具体行为,例如记录日志、持久化、等待一段时间再重新尝试等。
- 开发人员也可以通过实现
在使用线程池时,选择合适的拒绝策略非常重要,它可以影响到任务的执行和系统的稳定性。根据实际情况,选择合适的拒绝策略可以更好地处理任务的拒绝情况。
线程池的队列有几种
在Java中,线程池的任务队列是用来保存等待执行的任务的数据结构。Java中的线程池框架 ThreadPoolExecutor
提供了几种常见的任务队列实现,下面是其中几种:
-
直接提交队列(Direct handoff):
- 直接提交队列是一个没有容量限制的队列,任务提交时会立即交给工作线程执行,如果没有空闲的工作线程,则会创建新的线程来执行任务。这种队列的特点是任务提交和任务执行是同步的,不会缓存任务。
-
无界队列(Unbounded queue):
- 无界队列是一个容量无限大的队列,可以保存任意数量的任务。当任务提交时,如果没有空闲的工作线程,则任务会被放入队列中等待执行。由于队列容量无限,因此不会拒绝任务,但可能会导致内存占用过高。
-
有界队列(Bounded queue):
- 有界队列是一个具有固定容量的队列,可以保存一定数量的任务。当任务提交时,如果队列已满,则线程池会根据拒绝策略来处理任务,例如丢弃、等待一段时间再重试等。有界队列可以控制任务提交的速度,防止任务过多导致资源耗尽。
-
优先级队列(Priority queue):
- 优先级队列是根据任务的优先级来排序的队列,具有高优先级的任务会被优先执行。可以使用
PriorityBlockingQueue
类来实现优先级队列,它是一个线程安全的无界优先级队列。
- 优先级队列是根据任务的优先级来排序的队列,具有高优先级的任务会被优先执行。可以使用
-
链表阻塞队列(Linked blocking queue):
- 链表阻塞队列是一个基于链表结构的有界队列,可以保存一定数量的任务。它具有先进先出(FIFO)的特性,并且支持多线程并发操作,适用于生产者-消费者模式。
以上是几种常见的线程池任务队列实现。根据实际需求和性能考量,选择合适的队列实现可以更好地提高系统的性能和稳定性。
分布式锁
分布式锁是一种用于在分布式系统中控制共享资源访问的机制,它可以确保在多个节点上的不同线程或进程之间对共享资源的访问是互斥的。在实现分布式锁时,需要考虑到分布式系统中的网络通信延迟、节点故障等因素,以确保分布式锁的正确性和可靠性。下面是几种常见的分布式锁实现方式:
-
基于数据库的分布式锁:
- 使用数据库表来作为分布式锁的存储介质,利用数据库的事务特性和唯一约束来确保锁的唯一性。当需要获取锁时,向数据库中插入一条记录,并利用唯一约束来防止其他节点获取相同的锁。释放锁时,删除相应的记录。但是基于数据库的分布式锁通常会带来较高的数据库负载。
-
基于缓存的分布式锁:
- 使用分布式缓存系统(如Redis、Memcached等)来实现分布式锁。通过在缓存中设置锁的键值对,利用缓存的原子性操作(如SETNX、SETEX)来确保锁的唯一性。获取锁时,尝试设置锁的键值对,如果设置成功则表示获取锁成功;释放锁时,删除相应的键值对。
-
基于ZooKeeper的分布式锁:
- 利用ZooKeeper的临时顺序节点和Watch机制来实现分布式锁。每个节点创建一个临时顺序节点,获取锁时选择节点序号最小的节点作为锁的持有者,释放锁时删除相应的节点。通过Watch机制来监听节点的变化,当锁的持有者释放锁时,其他节点能够收到通知,从而重新竞争锁。
-
基于分布式锁框架:
- 也可以使用一些开源的分布式锁框架来实现分布式锁,例如Curator的InterProcessMutex、Redisson等。这些框架提供了封装良好的分布式锁实现,并且考虑了分布式系统中的一些特殊情况,使得使用者能够更方便地实现分布式锁。
在选择分布式锁实现方式时,需要根据实际情况综合考虑系统的复杂性、性能需求、一致性要求等因素。
mybatis使用for循环
<foreach item="item" index="index" collection="allIdList" open="(" close=")" separator=",">
#{item}
</foreach>
mybatis实现模糊查询两种方式
第一种方式:
如果您不想使用CONCAT
函数来拼接字符串,还可以通过#{}
占位符来实现。以下是一个示例,在Mapper XML文件中使用占位符实现模糊查询:
<!-- UserMapper.xml -->
<select id="findUsersByName" resultType="User">
SELECT * FROM User
<where>
<if test="name != null and name != ''">
AND name LIKE #{ '%' + name + '%' }
</if>
</where>
</select>
在这个示例中,我们直接使用#{}
占位符,并通过字符串拼接来构建模糊查询的条件。#{ '%' + name + '%' }
将会被解析为'%'+name+'%'
,即在传入的name参数两端加上%
,实现模糊查询。
第二种方式:
如果您想在MyBatis中使用动态SQL语句来实现模糊查询,您可以使用MyBatis提供的<if>
标签来动态生成查询条件。以下是一个示例:
在Mapper XML文件中:
<!-- UserMapper.xml -->
<select id="findUsersByName" resultType="User">
SELECT * FROM User
<where>
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
</where>
</select>
在这个示例中,<if>
标签用于检查传入的参数name是否为null或空字符串,如果不是,则在查询中添加模糊查询的条件。
这样就完成了使用MyBatis动态SQL语句进行模糊查询的操作。
串口通信和并口通信有什么区别
串口通信(Serial Communication)和并口通信(Parallel Communication)都是计算机用于与外部设备通信的方式,但它们之间存在一些重要的区别:
-
数据传输方式:
- 串口通信:串口通信是一种串行传输方式,数据位按照一位一位的顺序进行传输,通常使用单根电缆(串口线)传输数据。
- 并口通信:并口通信是一种并行传输方式,数据位同时以多位并行传输,通常使用多根电缆(并口线)传输数据。
-
线数和引脚:
- 串口通信:串口通信一般使用较少的线数,通常包括数据线、控制线和地线。常见的串口类型包括RS-232和RS-485。
- 并口通信:并口通信需要更多的线数,因为每个数据位都需要单独的线。常见的并口包括打印机并口(如Centronics接口)和并行端口(Parallel Port)。
-
传输速度:
- 串口通信:由于是串行传输,串口通信的传输速度一般比并口通信慢。传输速度通常以波特率(Baud Rate)来衡量。
- 并口通信:并口通信由于并行传输,通常比串口通信快。然而,实际速度也受到设备和连接质量的影响。
-
应用领域:
- 串口通信:串口通信通常用于连接外部设备,如调制解调器、传感器、GPS接收器、嵌入式系统等。
- 并口通信:并口通信曾经用于连接打印机、扫描仪、外部存储设备等,但由于速度较慢和占用大量引脚等缺点,已逐渐被USB等通信方式取代。
总之,串口通信和并口通信都是用于计算机与外部设备之间的数据传输,但它们在传输方式、线数、速度和应用领域等方面有明显的区别。通常情况下,选择通信方式取决于外部设备的类型、通信需求和可用的硬件接口。在现代计算机中,串口通信和并口通信已经不如以前常见,USB等通用接口更加普及。
Spring Security如何校验用户名和密码,流程是哪样的
- 首先登录接口接收到前端传参用户名和密码,将他们传到安全框架
UsernamePasswordAuthenticationToken
构造方法里面创建实例对象,再将该实例对象传到AuthenticationManager
的authenticate()
方法里,传递调用 - 需要定义一个类实现
UserDetailsService
接口,重写他的loadUserByUsername
方法,在该方法里,通过用户名在数据库中查询到对应的密码,将用户名和密码查询到封装起来,返给框架,框架去校验传参的密码和数据库的密码是否一致,如果不一致提示用户,如果一致,使用jwt生成token,返回给用户 - 业务访问时,携带token,过滤器会校验token是否合法,如果不合法就会拦截,过滤器的详细配置是jwt过滤器会实现
OncePerRequestFilter
接口并且重写doFilterInternal()
方法,在该方法里面拿到token,处理解析token,解析出来的数据没有问题就存到框架里SecurityContextHolder.getContext().setAuthentication()
- 将jwt过滤器注册到框架的配置当中,该框架的配置需要实现
WebSecurityConfigurerAdapter
这个抽象类并且重写configure()
方法,在该方法里配置jwt过滤器,比如http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//配置jwt过滤器,放在UsernamePasswordAuthenticationFilter前面 http.exceptionHandling()
Spring Security常见面试题
Spring Security 是一个广泛用于认证和授权的框架,因此在面试中可能会涉及到与 Spring Security 相关的问题。以下是一些常见的 Spring Security 面试题和答案:
-
什么是 Spring Security?
- Spring Security 是一个用于身份验证(Authentication)和授权(Authorization)的框架,用于保护应用程序的安全性。
-
Spring Security 的主要特性是什么?
- Spring Security 提供了诸多特性,包括身份验证、授权、会话管理、密码存储、CSRF 防护、注解支持等。
-
Spring Security 如何处理身份验证?
- Spring Security 使用过滤器链来处理身份验证,其中包括用户名和密码的验证、记住我功能、单点登录等。
-
Spring Security 支持哪些身份验证提供者?
- Spring Security 支持多种身份验证提供者,包括基于数据库的用户存储、LDAP、OAuth、OpenID、CAS 等。
-
如何配置基于用户名和密码的身份验证?
可以通过配置 UserDetailsService 和密码编码器(PasswordEncoder)来实现基于用户名和密码的身份验证
-
什么是权限(Authority)和角色(Role)?
- 权限是用户或角色的具体操作权限,而角色是权限的集合。Spring Security 使用角色来组织和管理权限。
-
如何配置基于角色的访问控制?
- 使用
@PreAuthorize
或@Secured
注解以及方法级别的安全性配置来实现基于角色的访问控制。
- 使用
-
什么是 CSRF 攻击?Spring Security 如何防御 CSRF 攻击?
- CSRF(跨站请求伪造)攻击是一种恶意网站伪造用户的请求。Spring Security 可以通过生成和验证令牌(CSRF Token)来防御CSRF攻击。
-
什么是单点登录(SSO)?Spring Security 支持单点登录吗?
- 单点登录是用户只需一次登录,就可以访问多个相关应用程序的机制。Spring Security 支持单点登录,可以与Spring Cloud Security一起实现。
-
Spring Security 和 OAuth 2.0 有何关系?
- Spring Security 可以与 OAuth 2.0 集成,用于实现安全的授权服务和保护资源服务器。
-
如何自定义登录页面和注销行为?
- 可以通过配置
loginPage()
和logoutUrl()
来自定义登录页面和注销行为。
- 可以通过配置
-
Spring Security 中的会话管理是什么?如何配置会话管理?
- 会话管理涉及到控制用户的会话,可以配置最大会话数、会话过期策略等。
-
什么是JWT(JSON Web Token)?Spring Security 支持 JWT 吗?
- JWT 是一种用于在网络应用之间传递信息的开放标准(RFC 7519)。Spring Security 支持 JWT,可以用于认证和授权。
-
如何在Spring Boot中配置Spring Security?
- 在Spring Boot中,可以使用
spring-boot-starter-security
依赖来快速配置Spring Security。可以通过application.properties
或application.yml
文件进行配置。
- 在Spring Boot中,可以使用
-
如何处理跨域请求(CORS)?Spring Security 如何支持CORS?
- 可以通过配置
CorsFilter
或使用 Spring Security 提供的@CrossOrigin
注解来处理跨域请求。
- 可以通过配置
这些问题涵盖了 Spring Security 的基本概念、功能和配置,但根据具体的职位和项目要求,面试官可能会提出更深入和复杂的问题,因此建议您深入学习 Spring Security 并准备好深入的理解和实践。
SpirngSecurity实现登录原理
Spring Security 使用一种称为身份验证提供者(Authentication Provider)的机制来验证用户名和密码是否正确。身份验证提供者是 Spring Security 的核心组件之一,负责验证用户的身份,并提供认证后的用户信息。
下面是 Spring Security 验证用户名和密码的一般过程:
-
用户登录:用户在登录页面上输入用户名和密码,然后提交登录请求。
-
拦截登录请求:Spring Security 拦截登录请求,并将其路由到相应的身份验证过滤器。
-
身份验证过滤器处理:身份验证过滤器会提取用户提供的用户名和密码,并封装成一个凭据对象(Credentials)。通常,这个凭据对象是
UsernamePasswordAuthenticationToken
的实例。 -
选择身份验证提供者:Spring Security 根据配置选择合适的身份验证提供者来验证用户的凭据。通常,身份验证提供者是
DaoAuthenticationProvider
,它使用UserDetailsService
来获取用户信息。 -
UserDetailsService 获取用户信息:
UserDetailsService
是一个接口,用于获取用户的详细信息。它从用户存储位置(如数据库)中检索用户的信息,包括用户名、密码和角色等。 -
验证用户名和密码:身份验证提供者使用
UserDetailsService
获取的用户信息与用户提供的用户名和密码进行比较。如果密码匹配,认证成功。 -
认证成功或失败:如果认证成功,Spring Security 会创建一个包含用户详细信息和授权信息的
Authentication
对象,并将其存储在安全上下文中。如果认证失败,将抛出异常。 -
重定向或授权:认证成功后,用户通常会被重定向到受保护的资源或授权页面,根据其角色和权限进行访问控制。
需要注意的是,身份验证提供者和用户详细信息的来源可以根据实际需求进行配置。通常情况下,UserDetailsService
从数据库或其他用户存储中获取用户信息。密码的存储和验证通常也由 Spring Security 处理,可以使用密码编码器(PasswordEncoder)来确保密码的安全性。
总之,Spring Security 提供了一个灵活且高度可配置的机制来验证用户名和密码,并支持多种身份验证提供者和用户详细信息源。这使得开发人员可以根据应用程序的需求选择合适的配置和实现方式。
Spirng框架用了哪些设计模式
Spring框架使用了多种设计模式,这些模式帮助Spring实现了各种功能和特性。以下是一些Spring框架中常用的设计模式:
-
单例模式(Singleton Pattern):Spring容器中的Bean默认是单例的,确保一个类只有一个实例。
-
工厂模式(Factory Pattern):Spring使用工厂模式来创建和管理Bean。BeanFactory和ApplicationContext负责创建和管理Bean实例。
-
依赖注入模式(Dependency Injection Pattern):Spring框架是依赖注入(DI)的鼻祖,它通过将依赖注入到Bean中来实现松耦合的组件之间的通信。
-
观察者模式(Observer Pattern):Spring的事件机制基于观察者模式,它允许Bean监听并响应应用程序中的事件。
-
模板方法模式(Template Method Pattern):Spring的JdbcTemplate和HibernateTemplate等模板类使用了模板方法模式,将通用的任务封装在模板方法中,子类可以覆盖特定的部分。
-
策略模式(Strategy Pattern):Spring中的AOP(面向切面编程)和事务管理使用了策略模式,允许开发人员定义横切关注点和切面。
-
装饰者模式(Decorator Pattern):Spring的AOP代理就是一种装饰者模式的应用,它在不改变原始类的情况下增加了额外的功能。
-
适配器模式(Adapter Pattern):Spring的适配器模式用于将不同类型的组件集成到Spring容器中,例如将非Spring Bean整合到Spring中。
-
建造者模式(Builder Pattern):Spring框架中使用建造者模式来配置和构建复杂的Bean定义,如Spring的
BeanDefinitionBuilder
。 -
代理模式(Proxy Pattern):Spring的AOP代理使用了JDK动态代理和CGLIB代理等代理模式来增强Bean的功能。
这些设计模式有助于Spring框架实现松耦合、可维护性、可扩展性和可重用性等关键特性,使Spring成为一个强大的企业级应用程序开发框架。它们帮助开发人员更容易地编写可维护和可测试的代码,提供了各种功能,如依赖注入、AOP、事务管理等,从而简化了应用程序的开发过程。
JDK动态代理和CGLIB代理有什么区别
JDK动态代理和CGLIB代理是两种常用的Java代理机制,它们在实现方式和适用场景上有一些区别:
-
实现方式:
-
JDK动态代理:JDK动态代理是基于Java反射机制实现的。它要求被代理的类必须实现一个接口,代理类通过实现同样的接口并在运行时生成代理对象。在代理对象的方法被调用时,会通过反射调用被代理对象的方法。
-
CGLIB代理:CGLIB(Code Generation Library)代理是通过字节码生成库实现的。它可以代理那些没有实现接口的类,通过继承被代理类,并覆盖其中的方法来创建代理对象。
-
-
适用场景:
-
JDK动态代理:适用于代理接口的情况。如果被代理的类实现了一个或多个接口,可以使用JDK动态代理。常见的应用场景包括AOP(面向切面编程)和代理事务管理。
-
CGLIB代理:适用于代理普通的Java类,包括没有实现接口的类。CGLIB可以代理具体的类,而不仅仅是接口。它常用于扩展现有类的功能,例如生成Bean的子类来实现延迟加载。
-
-
性能:
-
JDK动态代理:通常来说,JDK动态代理在性能上比CGLIB代理稍微快一些,因为它使用了Java原生的反射机制,但它要求被代理的类必须实现接口。
-
CGLIB代理:CGLIB代理通常比JDK动态代理慢一些,因为它需要生成和加载字节码,但它可以代理没有实现接口的类。
-
总之,选择JDK动态代理还是CGLIB代理取决于具体的需求和场景。如果被代理的类已经实现了接口,而且性能要求较高,可以考虑使用JDK动态代理。如果被代理的类没有实现接口,或者需要对类的方法进行扩展而不修改源代码,可以考虑使用CGLIB代理。在实际应用中,有时候也会结合两者的优点来使用。
常用的设计模式有哪些
创建型,结构型,行为行
常用的设计模式有很多,它们是经过反复验证和广泛应用的软件设计原则,可以帮助开发人员解决各种常见的设计问题。以下是一些常用的设计模式:
-
单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点。属于创建型设计模式
-
工厂模式(Factory Pattern):用于创建对象,但将对象的实际创建过程封装在工厂类中。属于创建型设计模式
-
抽象工厂模式(Abstract Factory Pattern):提供一组相关或依赖对象的接口,而不指定其具体类。属于创建型设计模式
-
建造者模式(Builder Pattern):用于创建复杂对象,将创建过程分解为一系列步骤。
-
原型模式(Prototype Pattern):通过复制现有对象来创建新对象,而不是从头开始构建。
-
适配器模式(Adapter Pattern):允许接口不兼容的类一起工作,提供一个中间适配器来转换接口。
-
装饰者模式(Decorator Pattern):允许动态地给对象添加新的行为,不需要修改其源代码。
-
代理模式(Proxy Pattern):提供一个代理对象来控制对其他对象的访问。属于结构型设计模式
-
观察者模式(Observer Pattern):定义对象间的一对多依赖关系,使得当一个对象改变状态时,所有依赖它的对象都会得到通知并更新。
-
策略模式(Strategy Pattern):定义一系列算法,将每个算法封装起来,并使它们可以相互替换。
-
命令模式(Command Pattern):将请求封装为一个对象,从而允许使用不同的请求、队列或者日志来参数化客户端。
-
状态模式(State Pattern):允许对象在内部状态改变时改变它的行为,看起来好像修改了它的类。
-
备忘录模式(Memento Pattern):捕获一个对象的内部状态,并在对象之外保存这个状态,以后可以将对象恢复到这个状态。
-
迭代器模式(Iterator Pattern):提供一种方法来访问一个聚合对象中的各个元素,而不需要暴露其内部表示。
-
组合模式(Composite Pattern):将对象组合成树形结构以表示“部分-整体”的层次结构。
-
模板方法模式(Template Method Pattern):定义一个算法的骨架,将一些步骤推迟到子类中实现。
-
访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,可以让你在不改变这些元素的类的前提下定义作用于这些元素的新操作。
-
解释器模式(Interpreter Pattern):给定一个语言,定义它的文法的一种表示,并定义一个解释器,用来解释语言中的句子。
这些设计模式可以帮助提高代码的可维护性、可读性和可扩展性,使得软件更容易维护和扩展。在实际开发中,根据具体的问题和需求选择合适的设计模式非常重要。
七层网络协议,是哪七层
七层网络协议模型是一种用于描述计算机网络协议的框架,也称为 OSI 模型(Open Systems Interconnection Model)。它将网络协议分为七个不同的层次,每个层次负责特定的功能,从物理传输到应用层面。以下是七层网络协议模型的每个层次:
-
物理层(Physical Layer): 这是最底层的层次,负责定义物理介质和电信号的传输方式,如电缆、光纤、电压等。它关注的是如何在物理层面传输比特流。例如串口通信属于这一层
-
数据链路层(Data Link Layer): 数据链路层负责将比特流组织成帧(Frame),并提供物理层的错误检测和纠正。它还处理局域网中的数据帧传输,以及物理地址(MAC 地址)的管理。
-
网络层(Network Layer): 网络层负责路由选择和数据包的转发。它定义了不同设备之间的路径选择算法,以及 IP 地址的管理。
-
传输层(Transport Layer): 传输层提供端到端的通信和数据传输服务。它包括了 TCP(传输控制协议)和 UDP(用户数据报协议)等协议,用于确保数据的可靠性和顺序传输。
-
会话层(Session Layer): 会话层负责建立、管理和终止通信会话。它处理了会话的开始、暂停、恢复和结束等操作。
-
表示层(Presentation Layer): 表示层处理数据的编码、解码和加密解密等操作,以确保数据在不同系统之间的兼容性。
-
应用层(Application Layer): 应用层包含了各种应用程序和协议,如HTTP、FTP、SMTP等,用于实现各种网络应用,例如网页浏览、电子邮件和文件传输。
这种七层模型的设计使不同的网络协议和功能可以被划分到不同的层次中,从而实现了协议的模块化和分层设计。这有助于不同厂商和组织之间的互操作性,并使网络协议的开发和维护更加可管理。虽然 OSI 模型在理论上非常有用,但实际上,互联网的协议体系更多地采用了 TCP/IP 模型,该模型包含了较少的层次,并将某些功能合并到了更少的层次中,但仍然保留了许多 OSI 模型的概念。
Tcp三次握手
在网络编程中,“握手”(Handshake)通常指的是建立连接的过程。TCP/IP 协议中的握手过程是为了确保通信双方都准备好进行数据传输。最常见的握手过程是 TCP 的三次握手,用于建立 TCP 连接。
以下是 TCP 的三次握手过程:
-
第一次握手(SYN): 客户端向服务器发送一个特殊的 TCP 报文,包含一个标志位 SYN(同步),表示客户端希望建立连接。客户端选择一个随机的初始序列号(ISN)作为起始值,并发送给服务器。
-
第二次握手(SYN-ACK): 服务器接收到客户端的请求后,会向客户端回复一个包含 SYN 和 ACK(确认)标志位的 TCP 报文,表示服务器同意建立连接。服务器也会选择一个随机的初始序列号,并将其发送给客户端。同时,服务器会确认客户端的序列号,将其加一作为确认号。
-
第三次握手(ACK): 客户端收到服务器的确认后,也会发送一个确认报文,其中包含 ACK 标志位。这个报文的序列号会加一,表示客户端已经确认了服务器的响应。
在完成这个三次握手过程后,双方就建立了连接,可以开始进行数据传输。这个握手过程确保了双方都准备好进行通信,同时也能够防止无效的连接请求。
需要注意的是,UDP 协议不涉及握手过程,因为它是一种无连接协议,数据包可以直接发送给目标,而不需要建立连接。握手通常在可靠性和有序性对数据传输要求较高的情况下使用,而 UDP 则用于那些可以容忍一些数据包丢失或乱序的应用。
LinkedList的底层是怎么实现的
LinkedList(链表)是一种常见的线性数据结构,它由一系列的节点组成,每个节点包含数据元素和指向下一个节点的引用。LinkedList的底层实现通常基于这种节点结构,具体取决于编程语言和标准库的实现。
在Java中,LinkedList的底层实现是双向链表(Doubly Linked List)。双向链表中的每个节点包含三部分信息:
- 数据元素:存储实际的数据。
- 指向下一个节点的引用:通常称为"next"或"nextNode",用于指示链表中的下一个节点。
- 指向前一个节点的引用:通常称为"previous"或"prevNode",用于指示链表中的前一个节点。
这种双向链表的结构使得在链表中进行前后遍历和节点插入/删除等操作更加高效,因为它允许直接访问前一个节点。
具体来说,Java的java.util.LinkedList
类使用双向链表来实现,它包含一个头节点和一个尾节点,以及指向链表中的前一个节点和后一个节点的引用。这使得在头部和尾部进行元素的添加和删除操作非常高效,但在随机位置插入和删除元素的效率较低。
下面是一个简化的示意图,表示双向链表的结构:
null <-> [Node 1] <-> [Node 2] <-> [Node 3] <-> ... <-> [Node N] <-> null
在这个示意图中,null
表示链表的起始和结束。每个节点包含数据和前后节点的引用。
需要注意的是,虽然Java的LinkedList
是双向链表的实现,但在某些编程语言中,也可以使用单向链表、循环链表等不同类型的链表来实现LinkedList。因此,具体的底层实现可以因编程语言和库的不同而异。
String拼接字符串为什么效率慢
String拼接字符串效率慢主要是因为String对象在Java中是不可变的(immutable),这意味着一旦创建了一个String对象,就不能再修改它的内容。因此,每次对String进行拼接、修改或连接操作时,都会创建一个新的String对象,而不是在原有的String上直接进行修改,这会导致性能问题,特别是在大量拼接字符串的情况下。
具体来说,以下是导致String拼接效率慢的主要原因:
-
创建新对象: 每次进行字符串拼接时,都会创建一个新的String对象,这会导致内存分配和垃圾回收的开销,特别是在循环中进行大量拼接操作时。
-
字符串不可变性: 由于String对象是不可变的,修改字符串内容必须创建一个新的String对象,将原有的内容复制到新对象中,这会导致额外的内存和时间开销。
-
性能退化: 随着字符串的拼接次数增加,性能会逐渐下降。这是因为每次拼接都需要复制已有字符串的内容,导致每次操作都比前一次操作慢。
为了提高字符串拼接的性能,通常可以采用以下方式:
-
使用StringBuilder或StringBuffer: 这两个类是可变的字符串缓冲区,它们允许在同一个对象上进行多次拼接操作,而不会创建新的String对象。StringBuilder是非线程安全的,适用于单线程环境,而StringBuffer是线程安全的,适用于多线程环境。
StringBuilder sb = new StringBuilder(); sb.append("Hello"); sb.append(" "); sb.append("World"); String result = sb.toString();
-
使用字符串格式化: 使用字符串格式化工具(如
String.format
或MessageFormat
)可以将变量插入到字符串中,而无需手动拼接。String name = "Alice"; int age = 30; String message = String.format("Name: %s, Age: %d", name, age);
-
避免在循环中频繁拼接: 如果需要在循环中拼接大量字符串,可以考虑将拼接结果存储在集合中,然后在循环结束后将它们连接成一个字符串,以减少对象创建次数。
总之,String拼接效率慢是因为String的不可变性,因此在处理大量字符串拼接时,最好使用StringBuilder或StringBuffer来提高性能。避免在循环中频繁拼接字符串,使用字符串格式化工具,都有助于优化字符串拼接操作。
Socket的原理是什么
Socket(套接字)是计算机网络编程中的一种通信机制,用于在不同计算机之间进行数据传输。Socket的原理基于网络协议栈和操作系统提供的接口,它允许程序通过网络连接进行数据的发送和接收。
Socket的工作原理包括以下关键步骤:
-
创建Socket: 在编程中,首先需要创建一个Socket对象,以便与其他计算机建立连接。Socket对象包含了通信所需的信息,如目标IP地址、端口号等。
-
建立连接: 一方作为服务器(Server Socket),另一方作为客户端(Client Socket)。服务器Socket在指定的IP地址和端口上监听客户端的连接请求。当客户端Socket发起连接请求时,服务器Socket接受连接,建立双向通信通道。
-
数据传输: 一旦连接建立,双方可以通过Socket进行数据传输。数据可以以字节流或字符流的形式在Socket之间传递。发送方将数据写入输出流,接收方从输入流中读取数据。
-
关闭连接: 数据传输完成后,可以关闭Socket连接。关闭连接会释放资源并终止连接,使Socket不再可用。
Socket的原理基于底层的网络协议栈,通常是TCP/IP协议栈。TCP/IP协议栈负责将数据分成小块(数据包),并确保它们以正确的顺序到达目标。Socket在应用层与传输层之间提供了一个接口,使开发人员能够轻松地进行网络通信。
在Java中,可以使用java.net
包中的Socket类来创建和管理Socket连接。这允许开发人员编写网络应用程序,实现客户端和服务器之间的通信。Socket编程是构建网络应用程序的基础,它可以用于各种网络通信需求,如Web服务器、聊天应用、文件传输等。
synchronized是公平锁吗
synchronized
是一种非公平锁(不保证公平性)。
在 Java 中,synchronized
关键字用于创建互斥锁,但它并不保证线程获取锁的顺序是公平的。也就是说,当多个线程竞争一个 synchronized
块或方法的锁时,JVM 并不会按照严格的顺序来分配锁,而是根据操作系统和线程调度的情况来分配。这意味着有可能某个线程一直获取到锁,而其他线程一直处于等待状态,导致不公平的竞争。
如果需要公平性,即按照等待时间的顺序分配锁,Java 提供了 ReentrantLock
类的构造函数可以选择创建公平锁。例如:
ReentrantLock lock = new ReentrantLock(true); // 创建公平锁
使用公平锁可以确保等待时间最长的线程优先获取锁,但需要注意,公平锁可能会降低锁的性能,因为它需要更多的线程切换开销。
总之,synchronized
不是公平锁,但可以通过其他手段实现公平锁的效果,例如使用 ReentrantLock
的公平锁构造函数。
synchronized几级锁
在 Java 中,synchronized
关键字可以应用于不同级别的锁,主要有以下几种级别的锁:
-
对象级别锁(对象锁): 这是最常见的锁级别,它锁定的是一个对象实例。当一个线程获取了某个对象的锁,其他线程必须等待该线程释放锁才能获得锁。例如:
public synchronized void synchronizedMethod() { // 这是对象级别锁,锁定的是当前对象 }
-
类级别锁(类锁): 类级别锁是在整个类上的锁,而不是某个对象实例上的锁。它适用于静态方法或静态代码块。当一个线程获取了类锁时,其他线程必须等待该线程释放锁才能获得锁。例如:
public static synchronized void synchronizedStaticMethod() { // 这是类级别锁,锁定的是类本身 }
-
块级别锁(局部锁): 这是一种更细粒度的锁,它锁定了某个特定的代码块,而不是整个方法或类。多个线程可以同时访问不同的代码块,只有在同一代码块内的线程之间会有竞争。例如:
public void someMethod() { // 这是块级别锁,锁定的是特定代码块 synchronized (lockObject) { // 代码块的内容 } }
这些不同级别的锁提供了不同的粒度,允许你在多线程环境中对代码进行更细粒度的同步控制。选择哪种锁取决于你的需求和设计,通常应根据需要确保线程安全性,同时尽量减小锁的范围,以提高性能。
ApplicationContext和BeanFactory的异同
ApplicationContext
和 BeanFactory
是 Spring 容器的两个重要接口,它们在应用程序中有一些异同点:
相同点:
-
Bean 的管理: 无论是
ApplicationContext
还是BeanFactory
,它们都负责管理和控制应用程序中的 Bean 对象。它们可以根据配置文件或注解来创建、配置和管理 Bean。 -
依赖注入: 无论是
ApplicationContext
还是BeanFactory
,它们都支持依赖注入,可以将依赖项自动注入到 Bean 中,实现松耦合的组件之间的协作。 -
作用域管理: 无论是
ApplicationContext
还是BeanFactory
,它们都支持管理 Bean 的作用域,如单例、原型等。 -
生命周期管理: 无论是
ApplicationContext
还是BeanFactory
,它们都可以管理 Bean 的生命周期,包括初始化和销毁阶段。
不同点:
-
初始化时机: 主要的不同点在于初始化时机。
BeanFactory
是懒加载的,它在第一次访问 Bean 时才进行初始化。而ApplicationContext
是在容器启动时就立即初始化所有的单例 Bean。 -
扩展性:
ApplicationContext
接口是BeanFactory
的扩展,它提供了更多的功能,如国际化支持、事件传播、资源加载、AOP 功能等。ApplicationContext
是BeanFactory
的超集,因此通常更常用。 -
资源管理:
ApplicationContext
提供了更丰富的资源管理功能,能够加载和管理不同类型的资源,如文件、类路径、URL 等。BeanFactory
主要关注 Bean 的管理,对资源管理的支持较少。 -
性能: 由于
ApplicationContext
在启动时会初始化所有单例 Bean,因此可能会占用更多的内存和时间。相比之下,BeanFactory
是懒加载的,只有在需要时才初始化 Bean,因此可能更节省资源。
总结:
ApplicationContext
和 BeanFactory
都是 Spring 容器的接口,它们在 Bean 的管理和依赖注入方面有很多相似之处,但在初始化时机、扩展性、资源管理和性能等方面存在一些不同。通常情况下,开发者更倾向于使用 ApplicationContext
,因为它提供了更多的功能和便利性。但如果对性能要求非常高,可以考虑使用 BeanFactory
。
Spring中BeanFactory和FactoryBean有什么区别?
BeanFactory
和 FactoryBean
是 Spring Framework 中两个不同的概念,它们用于不同的目的,有不同的作用。
-
BeanFactory:
BeanFactory
是 Spring 的核心容器接口之一,它负责管理和控制应用程序中的所有 Bean 对象。BeanFactory
提供了一种机制来实例化、配置和管理 Bean,并提供了 Bean 的生命周期管理、依赖注入等功能。BeanFactory
是 Spring 容器的基础接口,它定义了获取和管理 Bean 的标准方法,包括getBean()
方法来获取 Bean 实例。BeanFactory
是一个接口,它有多种实现,其中最常用的是ApplicationContext
接口,它是BeanFactory
的子接口,提供了更多的功能和扩展,如国际化支持、事件处理等。
-
FactoryBean:
FactoryBean
是 Spring 中的一个特殊接口,用于创建复杂的 Bean 对象或充当自定义 Bean 工厂。它允许开发者自定义创建和配置 Bean 的逻辑,从而可以生成复杂的 Bean 实例。FactoryBean
接口要求实现一个getObject()
方法,该方法返回一个 Bean 实例。通常,FactoryBean
的实现类会被注册到 Spring 容器中,然后通过容器的getBean()
方法获取FactoryBean
的实例,然后调用getObject()
方法来获取实际的 Bean。FactoryBean
可以用于延迟初始化、条件化创建 Bean,以及执行一些特定的逻辑来生成 Bean 实例。
总结:
BeanFactory
是 Spring 容器的核心接口,用于管理和控制 Bean 实例,而 FactoryBean
是一个接口,用于创建自定义的 Bean 工厂,允许开发者实现自定义的 Bean 创建逻辑。FactoryBean
的实现类可以生成特定类型的 Bean 实例,这些 Bean 实例可以是复杂的,也可以是延迟初始化的。
Springboot是怎么自动装载的
Spring Boot 的自动装载是通过 Spring 框架的 @Configuration
、@ComponentScan
、@EnableAutoConfiguration
等注解和条件注解来实现的。这一机制允许 Spring Boot 自动配置应用程序的依赖项,从而简化了应用程序的配置。
下面是 Spring Boot 自动装载的关键组件和原理:
-
@EnableAutoConfiguration: Spring Boot 应用程序的入口类通常会使用
@SpringBootApplication
注解,这个注解包含了@EnableAutoConfiguration
。@EnableAutoConfiguration
注解会启用自动配置机制,它会尝试根据项目的依赖和配置来自动配置应用程序的各种功能。Spring Boot 包含了大量的自动配置类,它们会根据需要添加到 Spring 应用程序上下文中。 -
条件注解: Spring Boot 使用条件注解来决定是否应该自动配置特定的组件。条件注解基于一组条件来判断是否满足自动配置的条件。例如,
@ConditionalOnClass
表示只有在类路径上存在某个特定类时才会应用自动配置。 -
自动配置类: Spring Boot 的自动配置是通过一组自动配置类实现的,这些类位于
org.springframework.boot.autoconfigure
包中。这些自动配置类使用条件注解和@Configuration
注解来配置应用程序的不同部分,例如数据库连接、Web服务器、消息队列等。 -
Spring Boot Starter: Spring Boot Starter 是一组预定义的依赖包,它们包含了常见的功能和组件,例如 Web、数据访问、安全等。当你添加一个 Starter 依赖时,Spring Boot 会自动为你配置这些功能的相关依赖和自动配置类。
-
application.properties/application.yml: Spring Boot 提供了配置文件,你可以在这些文件中定义自定义配置属性,以覆盖自动配置的默认值。通过这种方式,你可以定制应用程序的行为。
总结:
Spring Boot 的自动装载机制依赖于条件注解、自动配置类和应用程序的配置文件。它简化了应用程序的配置和依赖管理,使得开发者可以更轻松地构建和部署应用程序。这种自动装载机制是 Spring Boot 强大的特性之一,减少了繁琐的手动配置工作。
@SpringBootApplication 注解包含以下三个重要的注解
@SpringBootApplication
注解是 Spring Boot 中的一个复合注解,它包含了多个其他注解,用于简化 Spring Boot 应用程序的配置和启动。具体来说,@SpringBootApplication
注解包含以下三个重要的注解:
-
@Configuration
: 这个注解标记一个类作为配置类,它通常包含应用程序的配置信息。@SpringBootApplication
注解会将这个类注册为 Spring 上下文的一个 Bean。 -
@ComponentScan
: 这个注解指示 Spring 扫描并加载应用程序中的组件,包括控制器、服务、仓库等。它默认扫描与配置类相同的包及其子包下的组件。@SpringBootApplication
注解会自动配置包扫描,通常放置在主应用程序类上。 -
@EnableAutoConfiguration
: 这个注解启用了 Spring Boot 的自动配置功能。它会根据应用程序的类路径和依赖来自动配置 Spring 应用程序,包括数据源、Web服务器、消息队列等。@SpringBootApplication
注解会自动启用自动配置。
这三个注解的组合使得 @SpringBootApplication
成为一个强大的注解,它简化了 Spring Boot 应用程序的配置和启动过程,允许开发者快速搭建一个完整的 Spring Boot 应用程序。通过这个注解,开发者可以更专注于应用程序的业务逻辑而不必手动配置许多基础设施和组件。
介绍一下Redis
Redis(Remote Dictionary Server)是一种开源的内存数据库,也可以被称为缓存服务器,它提供了高性能的数据存储和检索功能。Redis 是一个基于键值对存储的数据结构服务器,具有以下特点:
-
内存数据库: Redis 将数据存储在内存中,这使得它具有极高的读写速度。数据也可以异步地持久化到磁盘上,以便在重启后恢复数据。
-
键值对存储: Redis 使用键值对(key-value)的方式存储数据,这种简单的数据模型非常适合缓存和快速检索。
-
多种数据结构支持: Redis 不仅支持简单的字符串,还支持多种数据结构,如哈希表、列表、集合、有序集合、位图等。这些数据结构可以让你更灵活地存储和操作数据。
-
持久化支持: Redis 可以将数据异步地持久化到磁盘上,以防止数据丢失。它支持两种持久化方式:快照(snapshot)和日志(append-only file)。
-
分布式支持: Redis 提供了集群模式,允许将数据分布到多个节点上,以实现高可用性和横向扩展。
-
发布-订阅: Redis 支持发布-订阅模式,允许客户端订阅消息通知,并在消息发布时接收通知。
-
事务支持: Redis 支持事务,可以一次性执行多个命令,保证这些命令的原子性。
-
Lua 脚本支持: Redis 支持使用 Lua 脚本执行复杂的操作,这可以减少网络开销并提高性能。
-
丰富的客户端库: Redis 提供了多种编程语言的客户端库,使得它可以轻松集成到各种应用程序中。
-
社区活跃: Redis 拥有庞大的开源社区,因此可以获取大量的文档、教程和支持。
Redis 主要用途包括:
- 缓存: Redis 最常用于缓存常访问的数据,以提高应用程序的响应速度。
- 会话管理: Redis 可以用于存储用户会话数据,实现分布式会话管理。
- 计数器和排行榜: Redis 可以用于实现计数器、排行榜和统计功能。
- 发布-订阅系统: Redis 的发布-订阅模式可用于实现消息队列和事件通知。
- 分布式锁: Redis 可以用于实现分布式锁,保证多个节点之间的协调操作。
- 地理信息处理: Redis 支持地理位置数据存储和查询,可用于构建地理信息应用。
- 实时分析: Redis 可用于实时数据分析和处理,如实时统计和日志分析。
总之,Redis 是一个多功能的、高性能的内存数据库,适用于各种不同的应用场景,尤其适合需要快速数据存储和检索的应用。
Redis的基本类型有哪些
Redis 是一个开源的高性能键值存储数据库,它支持多种不同的数据结构,每种数据结构都有不同的用途。以下是 Redis 支持的主要数据结构:
-
字符串(String):
- 字符串是最简单的 Redis 数据类型,可以存储文本、整数或二进制数据。可以执行一系列字符串操作,如设置值、获取值、追加、自增、自减等。
- 底层结构:简单动态字符串
-
哈希(Hash):
- 哈希是一个键值对集合,类似于Java中的Map。每个哈希可以存储多个字段和与其关联的值,用于表示对象或实体的属性。可以执行各种哈希操作,如设置字段值、获取字段值、删除字段等。
- 底层结构:哈希表和压缩列表
-
列表(List):
- 列表是一个有序的字符串元素集合,可以在列表的两端进行插入和删除操作。列表可以用于实现队列、堆栈等数据结构。
- 底层结构:双向链表,压缩列表
-
集合(Set):
- 集合是一个无序的字符串元素集合,每个元素都是唯一的。可以执行集合操作,如添加元素、删除元素、计算交集、并集、差集等。
- 底层结构:整数数组,哈希表
-
有序集合(Sorted Set):
- 有序集合是集合的扩展,每个元素都有一个关联的分数(score),用于排序元素。可以按分数范围获取元素,执行排名操作等。
- 底层结构:跳表,压缩列表
-
位图(Bitmap):
- 位图是一种特殊的字符串,可以存储位数据,用于位操作。可以进行位的与、或、非、计数等操作,用于处理二进制数据。
-
HyperLogLog:
- HyperLogLog 是一种概率数据结构,用于估计一个集合的基数(不重复元素的数量),具有较小的内存占用。
-
地理空间数据(Geospatial Data):
- Redis 支持存储和查询地理空间数据,用于地理位置应用和地理位置分析。
-
流(Stream):
- 流是Redis 5.0版本引入的数据结构,用于处理时间序列数据,支持发布-订阅模型、日志记录等场景。
这些不同的数据结构使Redis成为了一个多用途、灵活的数据存储系统,可以满足各种应用程序的需求。开发人员可以根据应用程序的需要选择合适的数据结构来存储和操作数据。
Spring中Bean的⽣命周期
在Spring中,Bean的生命周期包括多个阶段,每个阶段都可以由开发人员干预和定制。下面是Spring中Bean的详细生命周期:
-
实例化(Instantiation): 首先,Spring容器会创建Bean的实例。这通常涉及使用类的构造函数来创建一个新的对象。如果Bean配置中有工厂方法定义,那么这个工厂方法也可以用于创建Bean。
-
属性设置(Populating Properties): 一旦Bean实例被创建,Spring容器会注入或设置Bean的属性。这些属性可以是基本数据类型,其他Bean的引用,或集合属性。这是通过依赖注入(Dependency Injection)来完成的,Spring容器会根据Bean定义中的配置信息来自动完成属性注入。
-
Bean的初始化回调(Initialization Callbacks): 在属性设置之后,Spring容器会调用Bean的初始化回调方法。你可以通过以下方式定义初始化回调:
- 使用
@PostConstruct
注解标记一个方法,这个方法会在依赖注入完成后立即执行。 - 实现
InitializingBean
接口并覆盖afterPropertiesSet()
方法,这个方法会在属性设置后被调用。 - 在XML配置中,使用
init-method
属性指定初始化方法的名称。
初始化回调通常用于执行一些预处理操作,例如建立数据库连接、打开文件、初始化资源等。
- 使用
-
Bean的使用(Bean in Use): 此时,Bean实例已经可以在应用程序的其他部分使用了。它们处于活动状态,执行业务逻辑。
-
Bean的销毁回调(Destruction Callbacks): 当Bean的生命周期结束时(通常在容器关闭时),Spring容器会调用Bean的销毁回调方法。你可以通过以下方式定义销毁回调:
- 使用
@PreDestroy
注解标记一个方法,这个方法会在Bean被销毁前调用。 - 实现
DisposableBean
接口并覆盖destroy()
方法,这个方法会在Bean销毁前被调用。 - 在XML配置中,使用
destroy-method
属性指定销毁方法的名称。
销毁回调通常用于释放资源、关闭数据库连接、清理临时文件等。
- 使用
-
Bean的销毁(Destruction): 最后,Bean的实例被销毁,释放内存和资源。这发生在容器关闭时或当Bean的作用域结束时(例如单例Bean的生命周期随着容器的关闭而结束)。
总结:
Spring容器负责管理Bean的完整生命周期,开发人员可以通过初始化回调和销毁回调来插入自定义逻辑。这种生命周期管理有助于确保Bean在创建、使用和销毁过程中的一致性和可控性,同时减少了内存泄漏和资源泄漏的风险。
JDK 和 JRE 有什么区别?
JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境
JRE:Java Runtime Environment 的简称,java 运行环境,为 java 的运行提供了所需环境
具体来说 JDK 其实包含了 JRE,同时还包含了编译 java 源码的编译器 javac,还包含了很多 java 程序调试和分析的工具。简单来说:如果你需要运行 java 程序,只需安装 JRE 就可以了,如果你需要编写 java 程序,需要安装 JDK。
如何理解类
类是对现实生活中一类具有共同属性和行为的事物的抽象
类的特点:
•类是对象的数据类型
•类是具有相同属性和行为的一组对家的集合
3.类和对象的关系
如何理解封装
- 封装概述
是面向对象三大特征之一(封装,继承,多态)
是面向对象编程语言对客观世界的模拟,客观世界里成员变量都是隐藏在对象内部的,外界是无法直接操作的 - 封装原则
将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问
成员变量private,提供对应的getXxx()/setxxx()方法
3.封装好处
通过方法来控制成员变量的操作,提高了代码的安全性
把代码用方法进行封装,提高了代码的复用性
5.泛型是什么
ClassLoad是什么,解释一下原理和过程
类加载器是加载class文件到内存中,供其他class文件调用。
加载过程是:加载->链接(验证、准备、解析)->初始化->使用->卸载
ClassLoad(类加载器)是Java虚拟机(JVM)的一部分,它负责将Java字节码(编译后的Java源代码)加载到内存中并转换成可执行的Java类。类加载器是Java的一个重要概念,它允许应用程序在运行时动态加载类,这是Java语言的一个重要特性之一,也是实现Java的动态扩展和模块化的基础。
类加载器的原理和过程:
- 加载(Loading):在这个阶段,类加载器会查找并读取.class文件(Java字节码文件)。这些文件可以来自本地文件系统、网络、JAR文件等资源。类加载器会将字节码数据加载到内存中,并创建一个代表这个类的
java.lang.Class
对象。 - 链接(Linking):类加载过程的链接阶段包括三个步骤:
- 验证(Verification):在这一步,类加载器会验证加载的字节码是否符合Java虚拟机规范,以确保它是合法的、安全的。这包括类型检查、字节码验证、符号引用验证等。
- 准备(Preparation):在这一步,类加载器会为类的静态变量分配内存空间,并初始化这些变量的默认初始值(通常为零值)。这些静态变量被存储在类的静态存储区域中。
- 解析(Resolution):在这一步,类加载器会将符号引用替换为直接引用,以确保类、方法和字段的引用都能正确地连接到已加载的类或接口。这是一个可选步骤,不是所有类都需要解析。
- 初始化(Initialization):在这个阶段,类加载器会执行类的初始化代码,包括执行静态初始化块(
static
块)和静态字段初始化。这是类加载过程的最后一步,只有在初始化完成后,类才会真正准备好使用。
Java虚拟机提供了默认的类加载器,但也可以通过编写自定义类加载器来实现更复杂的加载策略。自定义类加载器通常用于实现类隔离、动态加载模块、热部署等功能。
总之,类加载器是Java中实现动态加载和运行时扩展的关键部分,它确保了Java的灵活性和可扩展性,允许应用程序在运行时根据需要加载新的类和资源。
JVM是什么?及原理
JVM (Java Virtual Machine) 是 Java 编程语言的关键组成部分之一,它是一个虚拟机,用于执行 Java 程序。JVM 的主要任务是将 Java 源代码编译成字节码(Bytecode),然后在运行时执行这些字节码。以下是 JVM 的一些重要特点和功能:
字节码执行:JVM 通过解释或即时编译(Just-In-Time Compilation,JIT)方式执行 Java 字节码。即时编译将字节码转换成本地机器代码,以提高执行速度。
平台无关性:Java 程序编写一次,可以在支持 JVM 的任何平台上运行,只要有相应的 JVM 实现。这个特性使得 Java 成为跨平台的编程语言。
内存管理:JVM 负责内存管理,包括分配和回收内存。它具有垃圾回收机制,可自动释放不再使用的内存,以减少内存泄漏的风险。
安全性:JVM 提供了一系列安全性功能,如字节码验证,以确保代码的合法性和防止恶意代码执行。
多线程支持:JVM 具有内置的多线程支持,可以轻松创建和管理多线程应用程序。
性能优化:现代的 JVM 实现包括各种性能优化技术,以提高 Java 程序的执行速度。
异常处理:JVM 提供了强大的异常处理机制,使开发人员能够有效地处理错误和异常情况。
类加载器:JVM 使用类加载器(Class Loader)来加载类和资源文件,它们可以动态地从不同的来源加载类,如本地文件系统、网络等。
总的来说,JVM 允许开发人员编写一次 Java 代码,并在不同的平台上运行,同时提供了许多强大的功能,包括内存管理、多线程支持和安全性,以简化和增强 Java 应用程序的开发和执行。不同的 Java 实现(例如Oracle HotSpot、OpenJ9、GraalVM等)提供了不同的性能特性和优化,以满足不同应用程序的需求。
JVM的生命周期:
1.启动。启动一个java程序的时候就产生了一个jvm实例
2.运行。Main方法是程序的入口,任何其他线程均有它启动
3.消亡。当程序中所有非守护线程都终止时,JVM才退出。若安全管理器允许,程序也可以使用Runtime类或者System.exit()来退出程序。
JVM的组成:
JVM是由类加载器,字节码执行引擎,运行时数据区(堆,栈,本地方法栈,方法区,程序计数器)组成的
JVM的优点:
一次编写跨平台运行
提供自动内存管理机制
Git提交代码命令
以下是一些常用的 Git 命令:
-
初始化仓库:
git init
: 在当前目录创建一个新的 Git 仓库。
-
克隆仓库:
git clone <仓库URL>
: 从远程仓库克隆一个副本到本地。
-
添加和提交文件:
git add <文件名>
: 将文件添加到暂存区。git add .
: 添加所有修改的文件到暂存区。git commit -m "提交消息"
: 提交暂存区的文件到本地仓库。
-
查看和比较变更:
git status
: 查看当前工作目录的状态。git diff
: 查看未暂存的变更。git diff --cached
: 查看已暂存的变更。git log
: 查看提交日志。
-
分支操作:
git branch
: 查看本地分支列表。git branch <分支名>
: 创建新分支。git checkout <分支名>
: 切换到指定分支。git merge <分支名>
: 合并指定分支到当前分支。git pull
: 拉取远程分支的更新。
-
远程仓库操作:
git remote -v
: 查看远程仓库列表。git remote add <远程名> <仓库URL>
: 添加远程仓库。git push <远程名> <分支名>
: 推送本地分支到远程仓库。git pull <远程名> <分支名>
: 拉取远程分支的更新。
-
撤销和重置:
git reset HEAD <文件名>
: 从暂存区中取消暂存文件。git checkout -- <文件名>
: 撤销对文件的修改(谨慎使用,会丢失未提交的修改)。
-
标签操作:
git tag
: 查看标签列表。git tag <标签名>
: 创建标签。git tag -a <标签名> -m "标签描述"
: 创建带有描述的标签。git push origin <标签名>
: 推送标签到远程仓库。
-
其他操作:
gitignore
文件:定义忽略的文件和目录。git stash
: 暂存当前工作目录的变更,以便切换到其他分支。git stash pop
: 恢复之前暂存的变更。
这些是 Git 的一些常用命令,可以帮助你进行版本控制、协作开发和管理代码仓库。根据你的具体需求,还有更多高级的 Git 命令和操作可以掌握。你可以使用 git --help
命令来查看 Git 的帮助文档,或查阅 Git 官方文档以获取更多信息。
Linux常用命令和基本操作
以下是一些常用的 Linux 命令和基本操作:
-
登录和注销:
- 登录系统:使用
ssh
命令连接到远程服务器,例如:ssh username@hostname
。 - 注销:使用
logout
或exit
命令退出当前用户会话。
- 登录系统:使用
-
文件和目录操作:
- 查看当前目录:
pwd
。 - 列出目录内容:
ls
,例如:ls -l
以长格式显示。 - 切换目录:
cd
,例如:cd /path/to/directory
。 - 创建目录:
mkdir
,例如:mkdir new_directory
。 - 复制文件或目录:
cp
,例如:cp file1 file2
或cp -r directory1 directory2
。 - 移动或重命名文件或目录:
mv
,例如:mv oldname newname
。 - 删除文件或目录:
rm
,例如:rm file
或rm -r directory
。
- 查看当前目录:
-
文件查看和编辑:
- 查看文件内容:
cat
、more
、less
或head
、tail
。 - 文本编辑器:使用
vi
或nano
进行文本编辑。 - 示例:
vi filename
进入 vi 编辑器,按i
进入编辑模式,完成后按Esc
,再输入:wq
保存并退出。
- 查看文件内容:
-
文件权限和所有权:
- 查看文件权限:
ls -l
。 - 更改文件权限:
chmod
,例如:chmod 755 filename
。 - 更改文件所有者和组:
chown
和chgrp
。
- 查看文件权限:
-
压缩和解压缩文件:
- 压缩文件:
tar
,例如:tar -czvf archive.tar.gz file1 file2
。 - 解压缩文件:
tar
,例如:tar -xzvf archive.tar.gz
。
- 压缩文件:
-
查找文件和目录:
- 查找文件:
find
,例如:find /path/to/search -name filename
。 - 查找文本内容:
grep
,例如:grep "pattern" file
。
- 查找文件:
-
进程管理:
- 查看运行中的进程:
ps
,例如:ps aux
。 - 杀死进程:
kill
,例如:kill PID
,PID 是进程 ID。
- 查看运行中的进程:
-
系统信息:
- 查看系统信息:
uname -a
。 - 查看系统资源使用情况:
top
或htop
。 - 查看硬件信息:
lscpu
、lsblk
、lshw
。
- 查看系统信息:
-
用户和权限管理:
- 创建用户:
useradd
。 - 更改用户密码:
passwd
。 - 添加用户到组:
usermod
。 - 切换用户:
su
或sudo
。
- 创建用户:
-
网络配置:
- 查看网络配置信息:
ifconfig
或ip addr
。 - 设置网络参数:
ifconfig
、ip
、route
。 - 测试网络连通性:
ping
。
- 查看网络配置信息:
这些是一些常用的 Linux 命令和基本操作,可以帮助你在 Linux 系统上进行常见的文件操作、系统管理和网络配置。根据你的需求和具体情况,可能会有更多命令和操作需要学习。
mybatis-plus比Mybatis相比,优点是什么
MyBatis:
所有SQL语句全部自己写
手动解析实体关系映射转换为MyBatis内部对象注入容器
不支持Lambda形式调用
Mybatis Plus:
强大的条件构造器,满足各类使用需求
内置的Mapper,通用的Service,少量配置即可实现单表大部分CRUD操作
支持Lambda形式调用
提供了基本的CRUD功能,连SQL语句都不需要编写
自动解析实体关系映射转换为MyBatis内部对象注入容器
Mybatis分页
借助数组进行分页
借助Sql语句进行分页
拦截器分页:MybatisPlusInterceptor
RowBounds实现分页
什么是io
从磁盘读到内存,从内存写到磁盘
按功能来分:输入流(input)、输出流(output)。
按类型来分:字节流和字符流。
字节流和字符流的区别是:字节流按 8 位传输以字节为单位输入输出数据,字符流按 16 位传输以字符为单位输入输出数据。
讲一下公平锁和非公平锁
公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
大量数据并发时,怎么解决
IOC和DI的区别
**IoC(Inversion of Control)和DI(Dependency Injection)**都是软件设计和开发中的重要概念,用于管理组件之间的关系和依赖,但它们有一些区别:
-
IoC(Inversion of Control):
- IoC是一种更广泛的概念,它是一种软件架构和设计原则,强调控制权的颠倒。
- IoC意味着将应用程序的控制权从应用程序本身转移到某个容器(通常是框架或容器)中,容器负责管理组件的生命周期、配置和依赖关系。
- IoC可以通过多种方式实现,其中包括依赖注入(DI)、事件驱动、服务定位等。
-
DI(Dependency Injection):
- DI是IoC的一种具体实现方式,是IoC的一种模式。
- DI强调组件之间的依赖关系是通过外部(通常是容器或框架)注入组件的方式来管理的。
- 在DI中,组件不再自己创建或获取它们所依赖的其他组件,而是将这些依赖关系声明为参数或属性,由外部容器负责注入依赖的对象。
总结:
- IoC是一种架构原则,它强调控制权的颠倒,让容器负责控制和管理组件。
- DI是IoC的一种具体实现方式,它强调通过外部注入依赖关系来管理组件之间的依赖。
- DI是IoC的一种模式,但不是唯一的IoC模式。其他IoC模式包括服务定位、事件驱动等。
- 在实际应用中,IoC和DI通常一起使用,DI是实现IoC的一种常见方式。
在现代软件开发中,特别是在使用框架和容器的情况下,IoC和DI已成为常见的设计和开发原则,它们有助于提高代码的可维护性、可测试性和松耦合性。
Mqtt通讯技术
List list = new ArrayList()和ArrayList list = new ArrayList();有什么不一样
第一个是面向接口,第二个是面向对象
将list里面的数组里数据过滤,只留下偶数
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FilterEvenNumbersInList {
public static void main(String[] args) {
// 创建一个包含数组的 List
List<Integer[]> list = new ArrayList<>();
list.add(new Integer[]{1, 2, 3, 4, 5});
list.add(new Integer[]{6, 7, 8, 9, 10});
list.add(new Integer[]{11, 12, 13, 14, 15});
// 使用 Java Stream 进行过滤
List<Integer[]> filteredList = list.stream()
.map(arr -> Arrays.stream(arr)
.filter(num -> num % 2 == 0)
.toArray(Integer[]::new))
.collect(Collectors.toList());
// 打印结果
filteredList.forEach(arr -> System.out.println(Arrays.toString(arr)));
}
}
集合分类
iMap,Collection两大类
Map分为hashMap和TreeMap
Collection分为Set和List
同步io和异步io
同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。
而异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。
&和&&的区别?
&
和 &&
都是在Java中用于执行逻辑与操作的运算符,但它们之间有一些重要的区别:
&
(按位与):&
是一个按位运算符,用于对两个整数值的二进制位进行逐位与操作。- 在布尔表达式中,
&
执行逻辑与操作,但不会进行短路求值。 - 即使第一个操作数的值为
false
,&
仍然会计算并评估第二个操作数。
示例:
int a = 5; // 二进制:0101
int b = 3; // 二进制:0011
int result = a & b; // 结果是1,二进制:0001
System.out.println("false & false 结果是"+(false & false));//结果是false
System.out.println("a | b 结果是"+(a | b));//有1是1 0111
System.out.println("a & b 结果是"+(a & b));//双1是1 0001
打印结果
false & false 结果是false
a | b 结果是7
a & b 结果是1
&&
(逻辑与):&&
是一个逻辑运算符,用于在布尔表达式中执行逻辑与操作。- 它进行短路求值,即如果第一个操作数的值为
false
,则不会继续评估第二个操作数,因为无论如何结果都将是false
。
示例:
boolean x = true;
boolean y = false;
boolean result = x && y; // 结果是false,不会评估y
总结:
&
是按位与运算符,可以用于整数运算和逻辑运算,不会短路求值。&&
是逻辑与运算符,仅用于逻辑运算,会进行短路求值,提高了程序的效率和安全性。
以下结果相等吗
Integer f1 = 100,f2 = 100,f3 = 150,f4 = 150;
System.out.println(f1 == f2);//true
System.out.println(f3 == f4);//false
如果字面量的值在-128到127之间,那么不会new新的Integer对象,而是直接引用常量池中的Integer对象,所以上面的面试题中f1f2的结果是true,而f3f4的结果是false。
System.out.println(2 >> 32)
是多少
是2
HashMap和HashSet的区别
HashMap
和HashSet
是Java集合框架中的两种不同的数据结构,它们有一些重要的区别:
-
数据结构:
HashMap
:是一种键值对(key-value)映射表,用于存储一组键值对,每个键都唯一,可以用于快速查找和访问值。HashSet
:是一种集合,用于存储一组唯一的元素,不允许重复值。
-
元素类型:
HashMap
:包含键值对,其中键和值可以是任意类型的对象。HashSet
:包含唯一的元素,通常是对象,但也可以包含基本数据类型的值,因为它们会被自动装箱。
-
存储方式:
HashMap
:使用哈希表来存储键值对,通过键的哈希码来定位值的存储位置,允许通过键来查找值。HashSet
:也使用哈希表来存储唯一元素,通过元素的哈希码来确定存储位置,允许通过元素来查找。
-
操作:
HashMap
:提供了put(key, value)
来插入键值对,get(key)
来根据键获取值,以及其他与键相关的操作。HashSet
:提供了add(element)
来添加元素,contains(element)
来检查是否包含特定元素,以及其他与元素相关的操作。
-
重复值:
HashMap
:允许不同键对应相同的值,但不允许重复的键。HashSet
:不允许重复的元素,如果试图添加重复的元素,它将被忽略。
-
迭代顺序:
HashMap
:不保证键值对的顺序,可以通过LinkedHashMap
来保持插入顺序。HashSet
:不保证元素的顺序,可以通过LinkedHashSet
来保持插入顺序。
-
线程安全性:
HashMap
和HashSet
都不是线程安全的。如果在多线程环境中使用它们,需要采取额外的同步措施或使用线程安全的集合类。
总之,HashMap
用于存储键值对,而HashSet
用于存储唯一元素。它们有不同的用途和适用场景,因此在选择使用哪个取决于您的需求。如果需要键值对映射,HashMap
更合适;如果需要存储唯一元素的集合,HashSet
更适合。
synchronized锁住的静态方法和普通方法有什么区别
synchronized
关键字用于实现多线程并发控制,可以用于锁定静态方法和非静态方法,但它们之间有一些关键区别:
-
锁定对象:
- 静态方法上的
synchronized
锁定的是类级别的锁,也称为类锁。这意味着不论多少个实例对象,它们都共享同一个类级别的锁,只有一个线程能够进入这个静态方法。 - 普通方法上的
synchronized
锁定的是实例级别的锁,每个实例对象都有自己的锁,因此多个实例对象可以并发执行各自的实例方法,不会互斥。
- 静态方法上的
-
锁定范围:
- 静态方法上的
synchronized
影响的是整个类,即无论调用哪个静态方法,都会互斥。 - 普通方法上的
synchronized
仅影响当前实例对象,不会阻止其他实例对象的方法调用。
- 静态方法上的
下面是一个示例来说明这两者之间的不同:
public class MyClass {
public synchronized void synchronizedMethod() {
// 这是一个实例方法,锁定的是当前实例对象
}
public static synchronized void synchronizedStaticMethod() {
// 这是一个静态方法,锁定的是整个类
}
}
使用 synchronized
静态方法可以控制对类级别资源的并发访问,而使用 synchronized
实例方法则可以控制对实例级别资源的并发访问。根据你的需求和具体情况,选择适合的锁定方式。需要注意的是,过多的锁定会导致性能问题,因此在设计并发程序时需要谨慎选择锁定的粒度。
Spring MVC的请求流程
Spring框架是一个广泛用于构建Java企业级应用程序的框架,其中Spring MVC(Model-View-Controller)是Spring框架的一部分,用于构建Web应用程序。以下是Spring MVC的请求处理流程:
-
客户端发起请求:
- 客户端(通常是浏览器)向Web服务器发送HTTP请求,请求特定的URL,例如
http://example.com/myapp/somePage
。
- 客户端(通常是浏览器)向Web服务器发送HTTP请求,请求特定的URL,例如
-
前端控制器(DispatcherServlet)接收请求:
- 在Spring MVC中,一个名为
DispatcherServlet
的前端控制器充当所有请求的入口点。DispatcherServlet
会拦截到所有请求,然后根据配置将它们路由到适当的处理程序(Controller)。
- 在Spring MVC中,一个名为
-
处理程序映射:
DispatcherServlet
使用处理程序映射器(Handler Mapping)来确定请求应该由哪个处理程序(Controller)来处理。处理程序映射器将请求URL映射到一个具体的处理程序。
-
处理程序执行:
- 一旦确定了请求的处理程序,
DispatcherServlet
调用相应的处理程序方法来执行业务逻辑。处理程序方法通常会访问模型(Model)来存储和获取数据,并返回视图(View)名称。
- 一旦确定了请求的处理程序,
-
业务逻辑执行:
- 处理程序方法执行业务逻辑,可能包括从数据库获取数据、处理请求参数、调用服务层等操作。
-
模型更新:
- 处理程序方法可以将数据添加到模型中,以便在视图中显示。模型是一个数据容器,通常包含在视图中渲染的数据。
-
视图解析:
DispatcherServlet
使用视图解析器(View Resolver)来解析处理程序返回的视图名称,以确定要使用哪个视图模板来呈现响应。
-
视图呈现:
- 视图模板负责将模型中的数据渲染到HTML页面或其他响应格式中,生成最终的响应内容。
-
响应发送:
DispatcherServlet
将视图生成的响应发送回客户端,通常是浏览器。这是通过HTTP响应完成的。
-
请求生命周期结束:
- 整个请求-响应周期结束后,请求生命周期也随之结束。
Spring MVC提供了丰富的配置选项,允许您自定义处理程序、拦截器、视图解析器等,以适应不同的应用程序需求。这个流程提供了一个清晰的结构,使得构建和维护Web应用程序变得更加容易。
以下结果 += 和 + 不同
byte c = 2;
c += 1;//合法 相当于c = (byte)(c + 1);
byte a = 1;
a = a + 1;//不合法 这里结果是int类型
int g = a + 1;//合法
索引有哪几种
在MySQL中,有几种常见的索引类型,每种类型都有其特定的用途和性能特点。以下是MySQL中常见的索引类型:
-
B-tree 索引:B-tree(平衡树)索引是MySQL中最常用的索引类型。它适用于各种查询,包括等值查询、范围查询和排序。B-tree索引可以用于所有存储引擎,包括InnoDB和MyISAM。
-
哈希索引:哈希索引适用于等值查询,但不适用于范围查询或排序。它们通常在内存表上使用,例如MEMORY存储引擎。InnoDB存储引擎也支持自动创建哈希索引以加速特定类型的查询。
-
全文索引:全文索引用于在文本数据上执行全文搜索。它们主要用于MyISAM存储引擎,并且支持全文搜索功能,以查找匹配的文本。
-
空间索引:空间索引用于处理空间数据类型,例如地理信息系统(GIS)数据。MySQL支持用于空间索引的特定类型,如R-tree。
-
前缀索引:前缀索引允许您只索引列值的一部分。这可以用于减小索引的大小,但也会降低查询性能。前缀索引通常用于处理大文本列。
-
复合索引:复合索引是包含多个列的索引。它们用于优化需要多个列的查询,例如多列的等值查询或范围查询。复合索引的列顺序非常重要,因为它们影响查询性能。
-
唯一索引:唯一索引确保索引列的值是唯一的,不允许重复值。这种索引用于实现唯一性约束,以确保表中的数据不包含重复记录。
-
主键索引:主键索引是一种特殊的唯一索引,它唯一标识表中的每一行,并且每个表只能有一个主键索引。主键索引通常与主键列关联,用于快速查找和引用表中的特定行。
-
外键索引:外键索引是用于实现外键关系的索引。它们用于维护表之间的引用完整性,确保在关联表中的数据一致性。
-
全列索引:全列索引是包含表中所有列的索引。通常,它们用于覆盖索引,以避免回表操作,从而提高性能。
每种索引类型都有其适用场景和性能考虑因素,根据您的查询需求和数据模型来选择适当的索引类型非常重要。不正确的索引策略可能会导致性能问题。
B-tree索引怎么使用
B-tree(平衡树)索引是MySQL中最常用的索引类型之一,用于加速各种查询操作,包括等值查询、范围查询和排序。下面是如何使用B-tree索引的一些示例:
-
创建B-tree索引:
您可以在表的列上创建B-tree索引,以加速对该列的查询。例如,假设您有一个名为
users
的表,并且要在username
列上创建索引:CREATE INDEX idx_username ON users(username);
这将在
username
列上创建一个B-tree索引,以加速与username
相关的查询。 -
等值查询:
B-tree索引特别适用于等值查询,例如:
SELECT * FROM users WHERE username = 'john_doe';
在这种情况下,MySQL可以使用B-tree索引快速查找
username
等于’john_doe’的行。 -
范围查询:
B-tree索引还可以用于范围查询,例如:
SELECT * FROM orders WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31';
在这种情况下,MySQL可以使用B-tree索引快速查找
order_date
在指定范围内的订单。 -
排序:
B-tree索引也有助于加速排序操作,因为它们存储了索引列的数据的有序副本。例如,如果您需要按
user_id
列对用户表进行升序排序:SELECT * FROM users ORDER BY user_id ASC;
B-tree索引可以加速这个排序操作。
-
覆盖索引:
如果您的查询只需要从索引中获取数据,而不需要回到表中检索其他列的值,那么可以使用覆盖索引来提高性能。例如:
SELECT username FROM users WHERE user_id = 123;
如果在
user_id
列上有一个B-tree索引,并且只需获取username
列的值,MySQL可以完全使用索引,而不需要回到表中查找。
请注意,创建索引会增加插入、更新和删除操作的开销,因此需要根据查询需求和数据库的使用模式仔细考虑索引的创建。同时,还要定期维护索引以确保其性能。使用EXPLAIN
语句可以帮助您分析查询的执行计划,以确定是否有效地使用了B-tree索引。
B-tree 索引底层结构
B-tree(B树)是一种常用于数据库和文件系统中的索引结构,它的底层结构包括以下重要组成部分:
-
节点(Node): B-tree是由一系列节点组成的树结构。每个节点通常表示一个范围或键值范围,并包含了与这个范围相关的数据条目。在数据库中,通常是索引键和指向数据行的指针。
-
根节点(Root Node): B-tree的顶层节点称为根节点。根节点包含了对整个B-tree的引用。根节点通常是一个特殊的节点,它可以有多个子节点,用于分割数据范围。
-
内部节点(Internal Node): 除了根节点外,B-tree还包括内部节点。内部节点通常包含一组键值范围,用于确定从该节点分支到哪个子节点。内部节点不包含实际的数据,而是用于导航到数据的叶子节点。
-
叶子节点(Leaf Node): 叶子节点是B-tree的最底层节点,它们包含了实际的数据条目。在数据库中,叶子节点通常包含索引键和指向数据行的指针。叶子节点之间可以按照键值排序,以支持范围查询。
-
平衡: B-tree通常是平衡的,这意味着从根节点到叶子节点的最长路径和最短路径的长度之差是有限的。这种平衡性使得B-tree的查询、插入和删除操作都具有良好的性能。
-
分支因子(Branching Factor): B-tree的每个内部节点通常有多个子节点,这个子节点的数量称为分支因子。分支因子决定了B-tree的层数和容量。
B-tree索引的主要优点之一是它的高效性能,特别适用于范围查询和区间查询。由于B-tree的平衡性和分支因子的设计,查询和维护操作的时间复杂度通常是O(log n),其中n是B-tree中的节点数量。
B-tree索引广泛用于关系型数据库管理系统(RDBMS)中,例如在MySQL、PostgreSQL、Oracle等数据库中。这些数据库使用B-tree索引来加速查询操作,提高数据检索性能。此外,B-tree的变种,如B+树和B*树,也被广泛使用,它们在某些场景下进一步优化了性能和存储效率。
如何知道正确使用了索引
使用MySQL索引是优化数据库性能的重要步骤之一,但需要注意一些重要事项,以确保索引的正确性和有效性。以下是使用MySQL索引时需要注意的事项:
-
选择合适的列: 索引应该选择那些经常用于检索数据的列,通常是
WHERE
子句中使用的列、JOIN
操作中连接的列、ORDER BY
子句中用于排序的列和GROUP BY
子句中用于分组的列。 -
避免不必要的索引: 不要为每个列都创建索引,只创建必要的索引。过多的索引会增加数据维护的开销,降低插入、更新和删除操作的性能。
-
使用索引覆盖查询: 如果查询只需要从索引中获取数据,而不需要访问实际的数据行,这种情况下称为索引覆盖查询,可以提高查询性能。
-
考虑复合索引: 复合索引包括多个列,可以提高特定查询的性能。但要注意不要创建过于复杂的复合索引,以避免降低插入和更新操作的性能。
-
使用前缀索引: 对于长文本列,可以考虑使用前缀索引,只索引文本的一部分字符,以减小索引的大小。
-
定期维护和优化索引: 定期分析和重建索引可以提高索引的性能。MySQL提供了
ANALYZE TABLE
和OPTIMIZE TABLE
等命令用于这些操作。 -
避免使用通配符开头的
LIKE
查询: 使用通配符开头的LIKE
查询(例如LIKE '%abc'
)无法使用索引,因为无法利用索引的前缀匹配优化。如果需要模糊查询,尽量避免通配符在开头。 -
小心使用索引提示: MySQL允许使用索引提示来强制查询使用特定的索引,但这应该谨慎使用。不正确的索引提示可能导致性能下降。
-
注意表结构的设计: 良好的表结构设计可以减少索引的需求。遵循范式规范,避免过度冗余数据,可以改善查询性能。
-
监控索引性能: 使用MySQL的性能监控工具来监视索引的使用情况,以及执行计划是否符合预期。
-
考虑存储引擎: 不同的MySQL存储引擎(例如InnoDB、MyISAM、Memory等)对索引的处理方式不同。选择存储引擎时要考虑索引的性能需求。
-
备份和恢复索引: 在备份和恢复数据库时,确保索引也得到了适当的处理,以防止数据丢失。
综上所述,使用MySQL索引需要谨慎考虑,结合具体的业务需求和查询模式来选择合适的索引策略。了解索引的工作原理以及监控索引性能是优化数据库性能的关键步骤。同时,定期维护和优化索引也是保持数据库高性能的关键因素之一。
使用MySQL的查询执行计划解释工具(EXPLAIN)可以帮助你了解查询是如何执行的,以便进行进一步的优化
索引不会包含有null值的列
只要列中包含有null值都将不会被包含在索引中,复合索引中只要有一列含有null值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为null。
使用短索引
对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个char(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。
索引列排序
查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。
like语句操作
一般情况下不推荐使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。
不要在列上进行运算
这将导致索引失效而进行全表扫描,例如
SELECT * FROM table_name WHERE YEAR(column_name)<2017;
不使用not in和<>操作
接口里面可以定义常量吗?
可以的
public interface Book {
String abc = "open";
Shop getShopName(Shop shop);
}
public class TestMain {
public static void main(String[] args) {
System.out.println(Book.abc);
}
}
结果是:open
请注意,尽管在接口中定义字段是合法的,但这种做法通常不被推荐,因为接口的主要目的是定义方法签名,而不是存储数据。通常情况下,应该使用常量类或枚举来定义常量,而不是将常量放在接口中。
接口之间可以多继承吗?用什么关键字?
在Java中,一个接口可以继承多个其他接口,这种多继承的机制是通过使用关键字 extends
来实现的。当一个接口继承多个其他接口时,它可以继承这些接口的方法定义,从而具备了多个接口的功能。
下面是一个示例,演示了如何在Java中实现接口之间的多继承:
interface A {
void methodA();
}
interface B {
void methodB();
}
// 接口C继承了接口A和接口B
interface C extends A, B {
void methodC();
}
class MyClass implements C {
@Override
public void methodA() {
System.out.println("Method A");
}
@Override
public void methodB() {
System.out.println("Method B");
}
@Override
public void methodC() {
System.out.println("Method C");
}
}
public class Main {
public static void main(String[] args) {
MyClass obj = new MyClass();
obj.methodA();
obj.methodB();
obj.methodC();
}
}
在上述示例中,接口 C
继承了接口 A
和接口 B
,因此它包含了这两个接口的方法定义。类 MyClass
实现了接口 C
,因此它需要实现接口 C
中的所有方法,以及接口 A
和接口 B
中的方法。
总结起来,Java中的接口之间可以多继承,使用 extends
关键字来实现多继承关系。这种方式允许你将多个接口的功能组合到一个接口中,从而提供更灵活的接口设计和实现。
接口可以实现抽象类吗?
不能,反之是可以的,抽象类可以实现抽象方法
在Java中,接口(interface)和抽象类(abstract class)是两种不同的概念,它们有不同的用途和特性。
-
接口(Interface):接口是一种抽象类型,它可以包含抽象方法的声明,但不能包含方法的实现。接口通常用于定义一组方法签名,而不提供具体的实现。类可以实现(implements)一个或多个接口,实现接口的类必须提供接口中声明的所有方法的实现。
-
抽象类(Abstract Class):抽象类也是一种抽象类型,但它可以包含抽象方法的声明以及方法的实现。抽象类用于定义具有一些通用实现的类,但允许派生类覆盖(override)其中的方法,也可以包含非抽象的方法。
接口不能直接实现抽象类,因为它们是不同的概念。一个类可以同时实现一个接口并扩展一个抽象类,这是允许的。例如:
// 接口
interface MyInterface {
void methodA();
}
// 抽象类
abstract class MyAbstractClass {
abstract void methodB();
}
// 实现接口并扩展抽象类的类
class MyClass extends MyAbstractClass implements MyInterface {
@Override
void methodB() {
// 实现抽象方法
}
@Override
public void methodA() {
// 实现接口方法
}
}
在上述示例中,MyClass
类同时实现了MyInterface
接口和扩展了MyAbstractClass
抽象类。它必须提供抽象类中的抽象方法和接口中的方法的实现。
总之,接口和抽象类都有其用途,它们可以在Java中用于不同的情景和需求。您可以根据具体的设计需求来选择使用哪个或同时使用两者。
Java中int型的数据能表示汉字吗
不能表示
汉字可以用char类型或者String类型表示
Java中int型数据能表示字母吗
可以表示
char letter = 'A'; // 字母字符 'A'
int intValue = (int) letter; // 将字母字符 'A' 转换为 int 值
System.out.println(intValue); // 输出 65
Java中的基本数据类型各占几个字节
在Java中,基本数据类型的大小是规定的,但具体的大小可能因不同的JVM实现和平台而有所不同。通常情况下,Java中的基本数据类型的大小如下:
- byte:占用1个字节(8位)。
- short:占用2个字节(16位)。
- int:占用4个字节(32位)。
- long:占用8个字节(64位)。
- float:占用4个字节(32位),但请注意,浮点数的精度和表示不同于整数。
- double:占用8个字节(64位),双精度浮点数的精度更高。
- char:占用2个字节(16位),用于表示Unicode字符。
- boolean:通常不明确规定大小,但其存储大小通常是虚拟机和平台相关的。
需要注意的是,这些基本数据类型的大小是Java语言规范中定义的标准大小。然而,不同的Java虚拟机(JVM)实现和不同的平台可能会有微小的差异,但这些差异通常很小,不会对大多数Java程序产生显著影响。
此外,Java的基本数据类型在内存中的布局也可以受到虚拟机的实现和平台的影响,例如,字节对齐等。因此,如果需要严格控制内存布局,可以考虑使用Java的ByteBuffer
等机制。
将本地文件上传到git空仓库
初始化项目
git init
将当前目录下所有需要上传的文件代码等资源添加到缓存区
添加一个或多个文件到暂存区:
git add [file1] [file2]
添加指定目录到暂存区,包括子目录:
git add [dir]
添加当前目录下的所有文件到暂存区:
git add .
提交缓存区里面的主要内容到本地仓库
git commit -m “提交说明”
添加一个远程仓库的地址
git remote add origin https://gitee.com/xxxxxxx(远程仓库的地址)
将远程仓库进行下拉,获取同步
git pull --rebase origin master
提交本地仓库到远程仓库
git push -u origin master
因为第一次提交 所以要加一个 -u
Mysql数据库的锁
表锁,页锁,行锁
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低,使用表级锁定的有MyISAM,MEMORY,CSV等一些非事务性存储引擎;
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高,使用行级锁定的主要是InnoDB存储引擎;
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般,使用页级锁定的主要是BerkeleyDB存储引擎。
对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁。
共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE
说两个维度:
共享锁(简称S锁)和排他锁(简称X锁)
读锁是共享的,可以通过lock in share mode实现,这时候只能读不能写。写锁是排他的,它会阻塞其他的写锁和读锁。从颗粒度来区分,可以分为表锁和行锁两种。
表锁和行锁
表锁会锁定整张表并且阻塞其他用户对该表的所有读写操作,比如alter修改表结构的时候会锁表。
行锁又可以分为乐观锁和悲观锁
悲观锁可以通过for update实现
乐观锁则通过版本号实现。
Mysql缓存
MySQL的缓存是用于提高数据库性能的重要组成部分,主要包括以下几种类型的缓存:
-
查询缓存(Query Cache): 查询缓存是MySQL中最基本的缓存类型,它可以缓存查询的结果集。当一个查询被执行后,MySQL会检查查询缓存,如果之前执行过相同的查询且缓存仍有效,就会直接返回缓存中的结果,而不需要再次执行查询。但要注意,查询缓存在MySQL 5.7版本及以后已经被弃用,不再推荐使用,因为它存在一些性能问题和限制。
-
缓冲池(Buffer Pool): 缓冲池是用于缓存数据页的内存区域。MySQL将磁盘上的数据分成数据页,当查询需要访问数据时,MySQL会首先查看缓冲池中是否已经加载了该数据页,如果存在就直接从内存中读取,而不需要访问磁盘。缓冲池的大小可以配置,较大的缓冲池可以提高查询性能。
-
键值缓存(Key-Value Cache): MySQL支持使用外部缓存系统,如Memcached或Redis,来缓存查询结果或数据。这些缓存系统通过将热门数据存储在内存中,可以显著提高读取操作的性能。MySQL可以与这些缓存系统集成,以支持缓存查询或数据。
-
表缓存(Table Cache): 表缓存用于存储表的元数据信息,例如表的结构、列的数据类型等。这些信息在执行查询和优化查询时经常被使用,因此缓存它们可以提高性能。
-
日志缓存(Log Cache): 日志缓存用于缓存二进制日志(binary logs)的内容,这对于主从复制等数据库复制操作非常重要。它可以加速数据同步过程。
-
查询结果缓存(Result Cache): 查询结果缓存是一种缓存查询的结果,而不是查询语句本身。与查询缓存不同,查询结果缓存通常更灵活,允许缓存不同参数的查询结果,但需要在应用程序中显式管理缓存。
-
存储过程和函数缓存: MySQL还可以缓存存储过程和函数的执行计划,以减少重复执行相同存储过程和函数的开销。
要有效地使用MySQL缓存,需要根据具体的应用和工作负载来进行适当的配置和调整。缓存的大小、清除策略、缓存命中率等都需要根据实际情况进行优化,以确保提高数据库性能。同时,还要注意监控缓存的使用情况,以及定期清理和维护缓存,以防止缓存过期或过度膨胀。
线程池什么是线程池?为什么要使用线程池?
线程池是一种用于管理和复用线程的机制,它可以预先创建一组线程,然后将任务分配给这些线程执行。通过使用线程池,可以避免频繁地创建和销毁线程,从而降低线程开销,提高系统性能和资源利用率。
Java 中如何创建线程池?请介绍一下 Executor 和 ExecutorService 接口。
在Java中,你可以使用Executor
框架来创建线程池,以管理和执行多个任务。线程池可以帮助你更有效地使用系统资源,并提高多线程应用程序的性能。以下是创建线程池的一些常见方式:
-
使用
Executors
工厂方法创建线程池:java.util.concurrent.Executors
类提供了一些工厂方法,用于创建不同类型的线程池。以下是一些示例:-
创建一个固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(5); // 创建一个包含5个线程的固定大小线程池
-
创建一个单线程的线程池:
ExecutorService executor = Executors.newSingleThreadExecutor(); // 创建一个单线程的线程池
-
创建一个缓存线程池:
ExecutorService executor = Executors.newCachedThreadPool(); // 创建一个根据需求自动调整线程数的线程池
-
-
手动创建线程池:
你也可以手动创建一个
ThreadPoolExecutor
,以更精细地控制线程池的属性,如核心线程数、最大线程数、线程空闲时间等。以下是一个示例:int corePoolSize = 5; int maxPoolSize = 10; long keepAliveTime = 5000; // 5秒 TimeUnit unit = TimeUnit.MILLISECONDS; BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(); ExecutorService executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, workQueue);
-
执行任务:
无论你选择哪种方式创建线程池,一旦创建完成,你可以使用
execute()
方法提交任务供线程池执行,或者使用submit()
方法提交任务并获取Future
对象以便跟踪任务的执行进度和结果。executor.execute(() -> { // 在这里执行你的任务逻辑 });
-
关闭线程池:
最后,不要忘记在程序结束时关闭线程池,以释放资源。
executor.shutdown(); // 关闭线程池
以上是创建和使用线程池的基本步骤。你可以根据自己的需求和项目的复杂性来选择不同类型的线程池,并配置线程池的属性以优化性能。确保在使用完线程池后关闭它,以避免资源泄漏。
Springboot创建定时任务几种写法
在Spring Boot中创建定时任务有多种写法,其中最常用的是使用@Scheduled
注解和实现SchedulingConfigurer
接口。以下是几种创建定时任务的写法:
-
使用@Scheduled注解:
使用
@Scheduled
注解是最简单的方式之一,它可以直接标注在需要定时执行的方法上。您可以在方法上设置定时任务的触发时间、周期、延迟等属性。import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class MyScheduledTasks { @Scheduled(fixedRate = 5000) // 每5秒执行一次 public void myTask() { // 定时任务的逻辑 } }
-
使用Cron表达式:
使用Cron表达式可以更灵活地定义定时任务的执行时间,例如每天凌晨执行、每周某一天的某个时间执行等。
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class MyScheduledTasks { @Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行 public void myTask() { // 定时任务的逻辑 } }
-
实现SchedulingConfigurer接口:
通过实现
SchedulingConfigurer
接口,您可以更灵活地配置定时任务。这种方式适用于需要动态配置定时任务的情况。import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; @Configuration @EnableScheduling public class MySchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addCronTask(() -> { // 定时任务的逻辑 }, "0 0 0 * * ?"); // 每天凌晨执行 } }
4.使用Quartz Scheduler:
使用 Quartz Scheduler 实现定时任务需要以下步骤:
-
引入 Quartz 依赖: 首先需要在项目中引入 Quartz 的依赖,以便使用 Quartz 的 API。可以通过 Maven、Gradle 或手动下载 jar 包的方式引入。
-
创建 Job 类: 创建一个类来实现定时任务的逻辑,该类需要实现
org.quartz.Job
接口,并实现execute()
方法。 -
配置 JobDetail: 配置
JobDetail
对象,指定要执行的 Job 类以及其它属性。 -
配置 Trigger: 配置触发器
Trigger
,指定定时任务的触发规则,如执行时间间隔、执行次数等。 -
配置 Scheduler: 创建
Scheduler
对象,并将JobDetail
和Trigger
关联起来。 -
启动 Scheduler: 启动
Scheduler
,定时任务将会按照配置的触发规则执行。
下面是一个简单的示例,演示了如何使用 Quartz Scheduler 实现一个定时任务:
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzSchedulerExample {
public static void main(String[] args) throws SchedulerException {
// 创建 Scheduler 实例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
// 创建 JobDetail
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "group1")
.build();
// 创建触发器 Trigger
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10) // 每隔10秒执行一次
.repeatForever()) // 一直重复执行
.build();
// 将 JobDetail 和 Trigger 关联起来
scheduler.scheduleJob(jobDetail, trigger);
}
// 自定义 Job 类
public static class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("定时任务执行:" + System.currentTimeMillis());
}
}
}
在这个示例中,我们创建了一个 Scheduler
实例并启动了它。然后,我们创建了一个 JobDetail
对象,指定要执行的 MyJob
类,并创建了一个触发器 Trigger
,指定了触发规则(每隔10秒执行一次)。最后,我们将 JobDetail
和 Trigger
关联起来,并将其加入到 Scheduler
中。
这样,定时任务就会按照指定的触发规则执行。
5.线程池
另一种创建线程池的写法是使用 ScheduledThreadPoolExecutor
类,它是 ScheduledExecutorService
接口的实现类,可以更灵活地配置线程池的参数。下面是使用 ScheduledThreadPoolExecutor
的示例代码:
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
executor.scheduleAtFixedRate(() -> {
System.out.println("定时任务执行:" + System.currentTimeMillis());
}, 0, 1, TimeUnit.SECONDS); // 每隔 1 秒执行一次
try {
Thread.sleep(5000); // 让程序运行 5 秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
在这个示例中,我们使用 ScheduledThreadPoolExecutor
的构造函数来创建一个大小为 1 的线程池,然后使用 scheduleAtFixedRate()
方法安排了一个定时任务,使其每隔 1 秒执行一次。与 Executors.newScheduledThreadPool()
方法相比,ScheduledThreadPoolExecutor
允许更灵活地配置线程池的参数,如核心线程数、最大线程数、线程空闲时间等。
InnoDB事务隔级别
InnoDB存储引擎支持多个事务隔离级别,这些隔离级别控制了事务之间的可见性和并发行为。不同的隔离级别提供不同的事务隔离程度,以满足不同的应用需求。以下是InnoDB支持的事务隔离级别:
-
READ UNCOMMITTED(读取未提交): 在这个最低级别的隔离中,一个事务可以读取另一个事务尚未提交的数据。这意味着事务之间没有任何隔离,可能会导致脏读(读取到未提交的数据)、不可重复读(同一查询在事务执行期间返回不同的结果)和幻影读(同一查询在事务执行期间返回不同数量的行)等问题。通常情况下,不推荐使用这个隔离级别。
-
READ COMMITTED(读取已提交): 这是大多数数据库系统的默认隔离级别,也是MySQL的默认隔离级别。在这个级别中,一个事务只能读取已经提交的数据,不会读取到未提交的数据。这解决了脏读问题,但仍然可能出现不可重复读和幻影读。
-
REPEATABLE READ(可重复读): 在这个隔离级别中,一个事务可以读取和锁定其他事务正在使用的数据,但不会读取未提交的数据。这可以防止脏读和不可重复读,但仍然可能发生幻影读。这是Innodb的默认隔离级别。
-
SERIALIZABLE(串行化): 这是最高的事务隔离级别,它确保了最高的隔离性。在这个级别中,事务是串行执行的,不会出现任何并发问题,包括幻影读。虽然它提供了最高的隔离性,但通常会导致性能下降,因为它限制了并发性。
你可以在MySQL中使用SET TRANSACTION ISOLATION LEVEL
语句来设置事务的隔离级别,例如:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
不同的应用场景可能需要不同的事务隔离级别。你需要根据应用的需求和性能要求来选择合适的隔离级别。通常情况下,READ COMMITTED或REPEATABLE READ是较常用的隔离级别。如果需要更高的隔离性,可以选择SERIALIZABLE,但要注意可能的性能影响。
Mysql的数据库事务四大特性
Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
@Autowired与@Resource注解的区别
@Autowired注解由Spring提供,只按照byType注入;@resource注解由J2EE提供,默认按照byName自动注入。
@Autowired功能虽说非常强大,但是也有些不足之处。比如:比如它跟spring强耦合了,如果换成了JFinal等其他框架,功能就会失效。而@Resource是JSR-250提供的,它是Java标准,绝大部分框架都支持。
除此之外,有些场景使用@Autowired无法满足的要求,改成@Resource却能解决问题。接下来,我们重点看看@Autowired和@Resource的区别。
@Autowired默认按byType自动装配,而@Resource默认byName自动装配。
@Autowired只包含一个参数:required,表示是否开启自动准入,默认是true。而@Resource包含七个参数,其中最重要的两个参数是:name 和 type。
@Autowired如果要使用byName,需要使用@Qualifier一起配合。而@Resource如果指定了name,则用byName自动装配,如果指定了type,则用byType自动装配。
@Autowired能够用在:构造器、方法、参数、成员变量和注解上,而@Resource能用在:类、成员变量和方法上。
什么是Redis,为什么用Redis?
Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。
读写性能优异
Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。
数据类型丰富
Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
原子性
Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
丰富的特性
Redis支持 publish/subscribe, 通知, key 过期等特性。
持久化
Redis支持RDB, AOF等持久化方式
发布订阅
Redis支持发布/订阅模式
分布式
Redis Cluster
Spring注入bean的值有几种方式
Set方法注入
构造器注入
静态工厂注入
实例工厂注入
@Value的用法
在使用上述配置文件时,可以直接@Value(“${user.userName}”)等等。
可以注入基本类型
实体字段有数据库没有的字段用哪些注解
@TableField(exist = false):表示该属性不为数据库表字段,但又是必须使用的。 @TableField(exist = true):表示该属性为数据库表字段。
Springboot的主要注解是哪个?有哪些注解组成
@SpringBootApplication
这是 Spring Boot 最最最核心的注解,用在 Spring Boot 主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力。
三个注解的组合,也可以用这三个注解来代替 @SpringBootApplication 注解。
@SpringBootConfiguration:@SpringBootConfiguration继承自@Configuration,二者功能也一致,标注当前类是配置类。
@EnableAutoConfiguration:启用Spring的自动加载配置。
@ComponentScan:该注解定义了Spring将自动扫描包主类所在包及其子包下的bean
如果你项目中所有的类都定义在主类包及其子包下,那你不需要做任何事,如果有别的类不在,需要注解后面配置当前包位置,和主类包位置
Spring定义bean的注解和装配bean的注解
定义:
@Component
@Bean
@Controller
@Service
@Repository
装备:
@Autowire:只按照类型
@Resources:默认按照名称找,找不到按照类型找
Java规范
阿里巴巴(Alibaba Group)有一套广泛使用的编程规范,被称为阿里巴巴Java开发手册。这个规范是基于阿里巴巴多年的软件开发经验和最佳实践建立的,旨在提高代码的质量、可维护性和可读性。以下是一些常见的阿里巴巴Java编程规范的要点:
-
命名规范:
- 类名、接口名使用大驼峰命名法(CamelCase)。
- 变量名、方法名使用小驼峰命名法(camelCase)。
- 常量名全部大写,用下划线分隔(例如:MAX_VALUE)。
- 包名使用小写字母,多级包名之间用
.
分隔(例如:com.alibaba.package)。
-
代码格式:
- 使用4个空格缩进,不使用制表符。
- 每行最大长度不超过80个字符。
- 使用Unix换行符(LF)。
- 在方法间空行隔开,类成员之间也要空行隔开。
-
注释:
- 使用Javadoc风格的注释来描述类、方法和字段。
- 为每个公共方法添加注释,描述方法的作用、参数、返回值以及可能的异常情况。
- 注释要求明确、简洁、规范,并保持及时更新。
-
异常处理:
- 捕获异常时,尽量精确指定捕获的异常类型,不要捕获通用的
Exception
。 - 不要在finally块中使用return语句。
- 捕获异常时,尽量精确指定捕获的异常类型,不要捕获通用的
-
避免使用魔法数值和魔法字符串,应该使用常量或枚举来代替。
-
类设计:
- 类应该满足单一职责原则,每个类应该有清晰的功能和目的。
- 类的字段应该声明为private,通过getter和setter方法访问。
-
包管理:
- 包的层次结构应该清晰,避免使用默认包。
- 避免使用通配符导入(例如:
import java.util.*
)。
-
并发编程:
- 使用线程池来管理线程,而不是直接创建线程。
- 合理使用同步机制,避免死锁和资源争夺。
-
性能优化:
- 避免不必要的性能消耗,如循环内部频繁的字符串拼接。
- 使用StringBuilder来进行字符串拼接而不是使用
+
操作符。
-
测试:
- 编写单元测试,确保代码的正确性。
- 使用Mockito等工具进行单元测试。
这些是阿里巴巴Java开发手册的一些要点,该手册还包含更多详细的规范和实践建议,以确保Java代码的质量和一致性。请注意,这些规范可能随着时间而变化,建议查阅阿里巴巴的官方文档以获取最新的信息和指导。
一个方法不能超过50行
复杂功能要写注释
格式化对其
抽取公共方法
数据库连接的表不能超过五个
@ComponentScan和@MapperScan的区别
首先,@ComponentScan是组件扫描注解,用来扫描@Controller @Service @Repository这类,主要就是定义扫描的路径从中找出标志了需要装配的类到Spring容器中
其次,@MapperScan 是扫描mapper类的注解,配置包的路径,就不用在每个mapper类上加@Mapper
反射的理解
在Java中,常用的反射方式主要包括以下几种:
-
获取Class对象: 获取Class对象是反射的起点,可以通过以下方式获取:
- 使用对象的
.getClass()
方法。 - 使用
.class
字面常量。 - 使用
Class.forName("完整类名")
方法。
下面是示例代码:
// 使用对象的 getClass() 方法 MyClass obj = new MyClass(); Class<?> clazz1 = obj.getClass(); // 使用 .class 字面常量 Class<?> clazz2 = MyClass.class; // 使用 Class.forName() 方法 Class<?> clazz3 = Class.forName("包名.MyClass");
- 使用对象的
-
获取构造函数并创建对象: 通过Class对象可以获取类的构造函数,进而创建对象。示例代码如下:
Class<?> clazz = MyClass.class; Constructor<?> constructor = clazz.getConstructor(); MyClass myObject = (MyClass) constructor.newInstance();
-
获取方法并调用: 使用Class对象可以获取类的方法,并通过反射调用方法。示例代码如下:
Class<?> clazz = MyClass.class; Method method = clazz.getMethod("methodName", parameterTypes); Object result = method.invoke(instance, arguments);
-
获取字段并设置/获取值: 通过反射可以获取类的字段,并设置/获取字段的值。示例代码如下:
Class<?> clazz = MyClass.class; Field field = clazz.getDeclaredField("fieldName"); field.setAccessible(true); // 如果字段是私有的,需要设置为可访问 Object value = field.get(instance); // 获取字段的值 field.set(instance, newValue); // 设置字段的值
-
操作注解: 反射可以用于操作类、方法、字段上的注解信息。示例代码如下:
Class<?> clazz = MyClass.class; // 获取类上的注解 MyAnnotation classAnnotation = clazz.getAnnotation(MyAnnotation.class); // 获取方法上的注解 Method method = clazz.getMethod("methodName", parameterTypes); MyAnnotation methodAnnotation = method.getAnnotation(MyAnnotation.class); // 获取字段上的注解 Field field = clazz.getDeclaredField("fieldName"); MyAnnotation fieldAnnotation = field.getAnnotation(MyAnnotation.class);
-
获取泛型类型信息: 反射可以用于获取类、方法、字段的泛型类型信息。示例代码如下:
Class<?> clazz = MyClass.class; // 获取类的泛型信息 Type[] genericTypes = clazz.getGenericInterfaces(); // 获取方法的泛型参数类型 Method method = clazz.getMethod("methodName", parameterTypes); Type[] parameterTypes = method.getGenericParameterTypes(); // 获取字段的泛型类型 Field field = clazz.getDeclaredField("fieldName"); Type fieldType = field.getGenericType();
需要注意的是,反射是一项强大但复杂的特性,应谨慎使用,因为它可能绕过编译时的类型检查,导致运行时异常。在使用反射时,应确保有充分的理由和必要性,并遵循最佳实践,同时保持代码的可读性和维护性。
72.并行和并发有什么区别?
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
在一台处理器上“同时”处理多个任务,在多台处理器上同时处理多个任务。如hadoop分布式集群。
所以并发编程的目标是充分的利用处理器的每一个核,以达到最高的处理性能。
73.线程和进程的区别?
进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程包含多个线程,一个线程只能属于一个进程。进程在执行过程中拥有内存单元。进程比线程消耗更多的资源。
74.暂定守护线程是什么?
守护线程(即daemon thread),准确地来说就是服务其他的线程。
创建线程有哪几种方式?
在Java中,有多种方式可以创建线程,以下是常见的几种方式:
-
继承Thread类: 这是一种创建线程的最基本方式,通过继承
Thread
类并重写run
方法来定义线程的执行逻辑。然后创建线程对象并调用start
方法启动线程。class MyThread extends Thread { public void run() { // 线程执行的逻辑 } } public class Main { public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // 启动线程 } }
-
实现Runnable接口: 这是一种更灵活的创建线程的方式,通过实现
Runnable
接口并将其作为参数传递给Thread
类来创建线程。这种方式支持多重继承,因为Java不支持多重继承。class MyRunnable implements Runnable { public void run() { // 线程执行的逻辑 } } public class Main { public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); // 启动线程 } }
-
使用匿名内部类: 可以使用匿名内部类来创建线程,特别适用于定义线程逻辑比较简单的情况。
Thread thread = new Thread(new Runnable() { public void run() { // 线程执行的逻辑 } }); thread.start(); // 启动线程
-
使用Java 8的Lambda表达式: 在Java 8及更高版本中,可以使用Lambda表达式来简化线程的创建。
Thread thread = new Thread(() -> { // 线程执行的逻辑 }); thread.start(); // 启动线程
-
使用线程池: Java提供了
Executor
框架和线程池来管理线程的创建和执行。线程池可以重复使用线程,减少线程的创建和销毁开销。ExecutorService executor = Executors.newFixedThreadPool(5); // 创建固定大小的线程池 executor.execute(() -> { // 线程执行的逻辑 }); executor.shutdown(); // 关闭线程池
不同的线程创建方式适用于不同的场景。通常情况下,推荐使用实现Runnable
接口或Lambda表达式的方式,因为它们更灵活,并且可以避免继承的局限性。线程池也是一种管理线程的好方法,可以提高线程的复用性和效率。选择合适的线程创建方式取决于你的需求和代码设计。
通过Callable和Future创建线程
创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
使用FutureTask对象作为Thread对象的target创建并启动新线程。
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
说一下 runnable 和 callable 有什么区别?
Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;
Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
线程池的几种状态
线程池有5种状态:Running、ShutDown、Stop、Tidying、Terminated。
线程有哪些状态?
在Java中,线程可以处于不同的状态,主要有以下几种线程状态:
-
New(新建): 当线程对象被创建但尚未调用其
start()
方法时,线程处于新建状态。在这个状态下,线程还没有被启动执行。 -
Runnable(可运行): 当线程对象调用了
start()
方法后,线程进入可运行状态。在这个状态下,线程已经被启动,但可能还未获得CPU的执行时间,因此它准备好了可以执行。 -
Blocked(阻塞): 线程在运行过程中可能会被阻塞,进入阻塞状态。这种情况通常是因为线程在等待某些条件满足,比如等待某个锁的释放。一旦条件满足,线程会从阻塞状态转移到可运行状态。
-
Waiting(等待): 线程进入等待状态,通常是因为调用了
wait()
方法,或者等待某个条件的发生。在等待状态下,线程不会消耗CPU时间,需要其他线程唤醒它。 -
Timed Waiting(计时等待): 与等待状态类似,但是等待有一定的时间限制。线程可以在等待一段时间后自动恢复到可运行状态,例如调用了
sleep()
方法或者join()
方法时。 -
Terminated(终止): 线程在执行完毕或者异常终止后进入终止状态。一旦线程处于终止状态,就不能再回到其他状态。当线程的
run()
方法执行结束时,或者线程抛出了未捕获的异常,线程会进入终止状态。
sleep() 和 wait() 有什么区别?
sleep():方法是线程类(Thread)的静态方法,让调用线程进入睡眠状态,让出执行机会给其他线程,等到休眠时间结束后,线程进入就绪状态和其他线程一起竞争cpu的执行时间。因为sleep() 是static静态的方法,他不能改变对象的机锁,当一个synchronized块中调用了sleep() 方法,线程虽然进入休眠,但是对象的机锁没有被释放,其他线程依然无法访问这个对象。
wait():wait()是Object类的方法,当一个线程执行到wait方法时,它就进入到一个和该对象相关的等待池,同时释放对象的机锁,使得其他线程能够访问,可以通过notify,notifyAll方法来唤醒等待的线程
notify()和 notifyAll()有什么区别?
如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。
当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争。
优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
线程的 run()和 start()有什么区别?
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程。
start()方法来启动一个线程,真正实现了多线程运行。这时无需等待run方法体代码执行完毕,可以直接继续执行下面的代码; 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
线程池的submit()和execute()有什么区别?
execute():只能执行 Runnable 类型的任务。
submit():可以执行 Runnable 和 Callable 类型的任务。
Callable 类型的任务可以获取执行的返回值,而 Runnable 执行无返回值。
synchronized在 Java 程序中怎么保证多线程的运行安全?
线程安全在三个方面体现:
原子性:提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic,synchronized);
可见性:一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile);
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序,该观察结果一般杂乱无序,(happens-before原则)。
多线程锁的升级原理是什么
在Java中,锁共有4种状态,级别从低到高依次为:无状态锁,偏向锁,轻量级锁和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。
Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了 “偏向锁” 和 “轻量级锁”:锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁可以升级但不能降级。
偏向锁:大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁。线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。
什么是死锁?
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
怎么防止死锁?
死锁的四个必要条件:
互斥条件:进程对所分配到的资源不允许其他进程进行访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放该资源
请求和保持条件:进程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但又对自己获得的资源保持不放
不可剥夺条件:是指进程已获得的资源,在未完成使用之前,不可被剥夺,只能在使用完后自己释放
环路等待条件:是指进程发生死锁后,若干进程之间形成一种头尾相接的循环等待资源关系
这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之 一不满足,就不会发生死锁。
理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和 解除死锁。
所以,在系统设计、进程调度等方面注意如何不让这四个必要条件成立,如何确 定资源的合理分配算法,避免进程永久占据系统资源。
此外,也要防止进程在处于等待状态的情况下占用资源。因此,对资源的分配要给予合理的规划。
ThreadLocal 是什么?有哪些使用场景?
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。
经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。
说一下 synchronized 底层实现原理?
synchronized 用的锁是存在 Java 对象头里的。在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
同步方法通过ACC_SYNCHRONIZED 关键字隐式的对方法进行加锁。当线程要执行的方法被标注上ACC_SYNCHRONIZED时,需要先获得锁才能执行该方法。
同步代码块通过monitorenter和monitorexit执行来进行加锁。当线程执行到monitorenter的时候要先获得锁,才能执行后面的方法。当线程执行到monitorexit的时候则要释放锁。每个对象自身维护着一个被加锁次数的计数器,当计数器不为0时,只有获得锁的线程才能再次获得锁。
Java中的每个对象都可以作为锁,锁的是当前实例对象。具体表现为以下三种方式:
对于普通方法,锁的是当前对象
对于静态同步方法,锁的是当前类的class对象
对于同步方法块,锁住的是synchronized里括号里配置的内容
synchronized关键字可以保证并发编程的三大特性:原子性、可见性、有序性,而volatile关键字只能保证可见性和有序性,不能保证原子性,也称为是轻量级的synchronized。
原子性:一个或多个操作全部执行成功或者全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源。
可见性:当一个线程对共享变量进行修改后,其他线程可以立刻看到。
执行synchronized时,会对应执行lock、unlock原子操作,保证可见性。
有序性:程序的执行顺序会按照代码的先后顺序执行。
synchronized关键字可以实现什么类型的锁?
悲观锁:synchronized关键字实现的是悲观锁,每次访问共享资源时都会上锁。
非公平锁:synchronized关键字实现的是非公平锁,即线程获取锁的顺序并不一定是按照线程阻塞的顺序。
可重入锁:synchronized关键字实现的是可重入锁,即已经获取锁的线程可以再次获取锁。
独占锁或者排他锁:synchronized关键字实现的是独占锁,即该锁只能被一个线程所持有,其他线程均被阻塞。
为什么说Synchronized是一个重量级锁?
Synchronized 是通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么 Synchronized 效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。
synchronized 和 volatile 的区别是什么?
1,作用的位置不同
synchronized是修饰方法,代码块。
volatile是修饰变量。
2,作用不同
synchronized,可以保证变量修改的可见性及原子性,可能会造成线程的阻塞;synchronized在锁释放的时候会将数据写入主内存,保证可见性;
volatile仅能实现变量修改的可见性,但无法保证原子性,不会造成线程的阻塞;volatile修饰变量后,每次读取都是去主内存进行读取,保证可见性
synchronized是修饰方法,代码块。
volatile是修饰变量。
synchronized 和 Lock 有什么区别?
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可);
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
synchronized 和 ReentrantLock 区别是什么?
Synchronized是关键字,ReentrantLock是类
ReentrantLock可以替代synchronized进行同步;
ReentrantLock获取锁更安全,不会无限等待,造成死锁;
必须先获取到锁,再进入try {…}代码块,最后使用finally(unlock())保证释放锁;
说一下 atomic 的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
Atomic系列的类中的核心方法都会调用unsafe类中的几个本地方法。我们需要先知道一个东西就是Unsafe类,全名为:sun.misc.Unsafe,这个类包含了大量的对C代码的操作,包括很多直接内存分配以及原子操作的调用,而它之所以标记为非安全的,是告诉你这个里面大量的方法调用都会存在安全隐患,需要小心使用,否则会导致严重的后果,例如在通过unsafe分配内存的时候,如果自己指定某些区域可能会导致一些类似C++一样的指针越界到其他进程的问题。
Docker常用命令
docker是一个用Go语言实现的开源项目,可以让我们方便的创建和使用容器,docker将程序以及程序所有的依赖都打包到docker container,这样你的程序可以在任何环境都会有一致的表现,这里程序运行的依赖也就是容器就好比集装箱,容器所处的操作系统环境就好比货船或港口,程序的表现只和集装箱有关系(容器),和集装箱放在哪个货船或者哪个港口(操作系统)没有关系。docker可以屏蔽环境差异,也就是说,只要你的程序打包到了docker中,那么无论运行在什么环境下程序的行为都是一致的
以下是一些常用的 Docker 命令,用于管理和操作 Docker 容器和镜像:
-
容器生命周期管理:
docker run <选项> <镜像>
: 创建并运行一个容器。docker start <容器ID或名称>
: 启动一个停止的容器。docker stop <容器ID或名称>
: 停止一个运行中的容器。docker restart <容器ID或名称>
: 重启一个容器。docker pause <容器ID或名称>
: 暂停一个运行中的容器。docker unpause <容器ID或名称>
: 恢复一个暂停的容器。docker rm <容器ID或名称>
: 删除一个容器。docker ps
: 列出运行中的容器。docker ps -a
: 列出所有容器,包括停止的容器。
-
镜像操作:
docker images
: 列出本地镜像列表。docker pull <镜像>
: 从 Docker Hub 或其他仓库拉取镜像。docker rmi <镜像ID或名称>
: 删除一个本地镜像。docker build -t <镜像名>:<标签> <Dockerfile路径>
: 基于 Dockerfile 构建镜像。docker tag <源镜像> <目标镜像>
: 给镜像打标签。
-
容器日志和执行命令:
docker logs <容器ID或名称>
: 查看容器的日志。docker exec -it <容器ID或名称> <命令>
: 在运行中的容器中执行命令。
-
容器网络和端口:
docker network ls
: 列出 Docker 网络。docker port <容器ID或名称>
: 查看容器的端口映射。docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' <容器ID或名称>
: 获取容器的 IP 地址。
-
容器数据管理:
docker volume ls
: 列出 Docker 数据卷。docker volume create <卷名称>
: 创建一个数据卷。docker volume rm <卷名称>
: 删除一个数据卷。
-
Docker Compose:
docker-compose up
: 启动 Docker Compose 项目。docker-compose down
: 停止并删除 Docker Compose 项目的容器。
-
Docker Swarm(集群):
docker swarm init
: 初始化 Docker Swarm 集群。docker service create <选项> <服务名称>
: 创建一个 Docker 服务。docker node ls
: 列出 Swarm 集群节点。
这些是一些常用的 Docker 命令,可用于管理容器和镜像,以及执行其他 Docker 相关操作。根据你的具体需求和项目,还有更多高级的 Docker 命令和配置选项可供使用。你可以使用 docker --help
命令来查看 Docker 的帮助文档,或查阅 Docker 官方文档以获取更多信息。
MySQL中的锁有哪些
MySQL 支持多种类型的锁,这些锁用于控制对数据库中的数据和表的访问。以下是 MySQL 中常见的锁类型:
-
共享锁 (Shared Lock,也称为读锁):
- 共享锁用于在一个事务中防止其他事务修改数据,但允许其他事务读取相同的数据。
- 当一个事务持有共享锁时,其他事务可以同时持有共享锁,但不能持有排他锁。
- 通过
SELECT ... FOR SHARE
或SELECT ... LOCK IN SHARE MODE
获取共享锁。
-
排他锁 (Exclusive Lock,也称为写锁):
- 排他锁用于在一个事务中防止其他事务读取或修改数据。
- 当一个事务持有排他锁时,其他事务不能持有任何锁。
- 通过
SELECT ... FOR UPDATE
获取排他锁。
-
行级锁 (Row-Level Lock):
- 行级锁允许不同事务同时锁定表中的不同行,这提高了并发性。
- MySQL 的 InnoDB 存储引擎支持行级锁。
-
表级锁 (Table-Level Lock):
- 表级锁锁定整个表,而不是表中的单个行。
- 表级锁会阻塞其他事务对整个表的访问,影响并发性能,应慎用。
-
意向锁 (Intention Lock):
- 意向锁是在行级锁和表级锁之间的一种锁,用于通知其他事务有意向获取行级锁或表级锁。
- 意向锁包括意向共享锁(IS,表示要获取共享锁)和意向排他锁(IX,表示要获取排他锁)。
-
自动锁定 (Auto-Locking):
- MySQL 使用自动锁定来管理事务中的锁,以确保数据的一致性。
- 在事务中插入、更新或删除数据时,MySQL 会自动为涉及的行和表添加适当的锁。
-
表级共享锁和表级排他锁:
- 在 MySQL 中,使用
LOCK TABLES
语句可以获得表级共享锁或表级排他锁,以控制对表的访问。 UNLOCK TABLES
语句用于释放表级锁。
- 在 MySQL 中,使用
-
死锁 (Deadlock):
- 死锁是指多个事务互相等待对方释放锁,从而无法继续执行的情况。MySQL 通常会自动检测并处理死锁。
在使用锁时,需要根据具体的业务需求和性能要求选择适当的锁策略。不正确的锁定策略可能会导致性能问题或死锁情况的发生。因此,在设计和实施数据库应用程序时,应特别注意锁的使用。
InnoDB事务隔级别
InnoDB 存储引擎支持四种事务隔离级别,分别是:
-
读未提交(Read Uncommitted): 这是最低的隔离级别。在这个级别下,一个事务可以读取另一个事务尚未提交的数据。这意味着一个事务可以看到其他事务中的脏读(Dirty Read),不可重复读(Non-Repeatable Read)和幻读(Phantom Read)问题。
-
读已提交(Read Committed): 这是InnoDB的默认隔离级别。在这个级别下,一个事务只能读取已经提交的数据,避免了脏读问题。但仍然可能会遇到不可重复读和幻读问题。
-
可重复读(Repeatable Read): 在这个级别下,一个事务在执行期间看到的数据保持一致,不会被其他事务修改。这避免了脏读和不可重复读问题,但仍可能会遇到幻读问题。
-
串行化(Serializable): 这是最高的隔离级别。在这个级别下,事务会按顺序执行,不会发生并发冲突。这避免了脏读、不可重复读和幻读等所有问题,但性能开销较大,通常不建议在高并发环境中使用。
在选择事务隔离级别时,需要权衡事务的一致性和性能之间的关系。较低的隔离级别通常会提高性能,但可能导致一致性问题。较高的隔离级别可以提供更强的一致性,但可能会降低性能。
可以使用SQL语句来设置事务隔离级别,例如:
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
请注意,不同的数据库管理系统可能支持不同的事务隔离级别,因此在使用时需要查阅相应数据库的文档以了解具体支持情况。此外,了解事务隔离级别对于编写具有并发访问的数据库应用程序非常重要,可以帮助避免各种并发问题。
Java中的序列化操作可以有哪些
在Java中,序列化是将对象转换为字节流的过程,以便可以将其保存到文件、数据库或通过网络传输。Java提供了一些用于序列化操作的核心类和接口,主要是以下几个:
- Serializable接口:要使一个类可序列化,只需让该类实现
java.io.Serializable
接口。这个接口是一个标记接口,没有需要实现的方法,它告诉Java运行时系统这个类是可序列化的。例如:
import java.io.Serializable;
public class MyClass implements Serializable {
// 类的成员和方法
}
- ObjectOutputStream和ObjectInputStream类:这些类用于将对象序列化为字节流和从字节流中反序列化对象。可以使用
ObjectOutputStream
将对象写入到输出流中,然后使用ObjectInputStream
从输入流中读取对象。例如:
// 序列化对象
MyClass obj = new MyClass();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.ser"));
oos.writeObject(obj);
oos.close();
// 反序列化对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.ser"));
MyClass newObj = (MyClass) ois.readObject();
ois.close();
- Externalizable接口:与
Serializable
接口不同,Externalizable
接口需要实现writeExternal
和readExternal
方法,以自定义对象的序列化和反序列化过程。这允许您更精确地控制对象的序列化和反序列化。示例:
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
public class MyClass implements Externalizable {
// 成员变量
private int data;
// 构造函数等其他方法
@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 自定义序列化过程
out.writeInt(data);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 自定义反序列化过程
data = in.readInt();
}
}
- Transient关键字:如果某个类的某个字段不希望被序列化,可以使用
transient
关键字修饰该字段,这样在序列化过程中,该字段的值将被忽略。例如:
public class MyClass implements Serializable {
private transient int nonSerializableField;
// 其他成员和方法
}
- 版本控制:为了确保反序列化的兼容性,可以使用
serialVersionUID
字段来定义类的版本号,以便在类发生变化时进行版本控制。例如:
private static final long serialVersionUID = 123456789L;
这些是Java中常见的序列化操作的关键组件和技术。通过使用这些类和接口,您可以有效地执行对象的序列化和反序列化操作。但是要注意,序列化可能涉及到版本控制、安全性等方面的考虑,具体情况取决于您的应用程序需求。
什么是规则引擎Drools
Drools 是一个开源的规则引擎系统,用于实现业务规则的管理和执行。它允许用户定义、管理和执行规则,以便在应用程序中实现灵活的决策逻辑和业务规则。以下是关于 Drools 的一些关键概念和功能:
-
规则引擎: Drools 是一个规则引擎,可以将业务规则从应用程序代码中分离出来,使规则可以独立管理和修改,而无需修改应用程序的源代码。这使得业务规则的维护和调整更加灵活和高效。
-
规则定义语言: Drools 使用自己的规则定义语言(DRL)来表示业务规则。DRL 是一种声明性的语言,允许用户描述条件、动作和规则之间的关系。规则可以包括条件部分(例如,当某些条件满足时)和动作部分(例如,执行某些操作)。
-
规则集合: 在 Drools 中,规则通常以规则集合的形式组织在一起,称为规则库(rulebase)。规则库中可以包含多个规则,这些规则可以相互关联和触发。
-
推理引擎: Drools 包括一个强大的推理引擎,用于匹配规则条件并执行相应的动作。当应用程序提供事实(facts)时,规则引擎会根据事实和规则来进行推理和决策。
-
决策表: Drools 还支持决策表,它是一种用于管理和执行决策规则的可视化方式。决策表允许非技术用户以表格形式定义规则,而无需编写规则代码。
-
集成能力: Drools 可以轻松集成到 Java 应用程序中,并支持与其他技术和框架(如 Spring)的集成。它还提供了 REST API 和 Web 控制台,用于管理和监视规则库的运行时状态。
-
开源和社区支持: Drools 是一个开源项目,拥有活跃的社区支持。它可以免费使用,且具有可扩展性和灵活性,可以满足各种业务规则管理的需求。
Drools 的主要用途包括业务规则管理、决策支持、复杂事件处理(CEP)、风险评估、定价和促销策略等领域。它为业务逻辑的管理和执行提供了一种强大的工具,帮助组织更好地应对不断变化的业务需求。
97.什么是spring
Spring是一个开源的、轻量级的Java应用框架,它提供了一系列的解决方案和工具,用于构建企业级应用程序和大规模应用的开发。Spring的核心理念是使Java开发更加简单、高效、可维护,并促进了面向对象编程的最佳实践。
以下是Spring框架的一些主要特性和功能:
-
依赖注入(Dependency Injection): Spring使用依赖注入来管理应用程序的组件(例如JavaBean)。它通过将依赖关系从代码中解耦,使应用程序更容易进行测试、维护和扩展。
-
面向切面编程(Aspect-Oriented Programming,AOP): Spring支持AOP,允许开发者定义横切关注点(cross-cutting concerns)并将它们应用到应用程序的不同部分。这有助于实现例如日志记录、事务管理和安全性等方面的功能。
-
事务管理: Spring提供了强大的事务管理支持,允许开发者使用声明性事务管理和编程式事务管理。它可以与多个不同类型的事务管理器(如JDBC、JTA、Hibernate等)集成。
-
集成其他技术: Spring框架可以与其他Java技术集成,如Java EE、JDBC、JMS、Hibernate、JPA、REST等,使得开发者可以更容易地构建全栈应用程序。
-
模块化: Spring框架被组织成多个模块,每个模块都有特定的功能,开发者可以根据需要选择性地使用这些模块,从而保持应用程序的轻量级和模块化。
-
简化测试: 由于Spring的依赖注入,使单元测试和集成测试更加容易。Spring提供了一系列的测试支持类,可以方便地编写和执行各种测试。
-
注解支持: Spring支持使用注解来配置和管理组件,使得配置更加简洁,代码更加清晰。
Spring框架的核心是IoC(Inversion of Control)容器,它管理应用程序中的对象生命周期和依赖关系。Spring IoC容器可以通过XML配置文件、Java注解或Java代码进行配置,以实现依赖注入和对象的创建与销毁。
总的来说,Spring框架为Java应用程序提供了一套强大的工具和功能,帮助开发者构建高质量、易于维护的企业级应用程序。Spring的灵活性和可扩展性使其成为广泛应用于企业级应用开发的首选框架之一。
DI(Dependency Injection)和IoC(Inversion of Control)是两个相关但不同的概念,它们通常一起使用,但它们的焦点和作用不同。
- DI(Dependency Injection,依赖注入):
- DI 是一种设计模式,用于管理组件之间的依赖关系。
- 在DI中,一个组件(通常是一个类或对象)不再负责创建或查找它所依赖的其他组件,而是通过外部的机制将这些依赖项注入到它内部。
- DI有助于降低组件之间的耦合度,使代码更易于测试、维护和扩展。
- DI可以通过构造函数注入、setter方法注入或接口注入等方式实现。
- IoC(Inversion of Control,控制反转):
- IoC是一种设计原则,强调将控制权从应用程序代码转移到框架或容器中。
- 在IoC中,应用程序不再控制组件的创建和生命周期,而是将这些控制权委托给IoC容器,容器负责创建、配置和管理组件。
- IoC的目标是解耦应用程序的各个组件,使其更灵活、可维护和可测试。
- DI是IoC的一种具体实现方式,它是IoC的一部分,用于实现依赖注入。
总结:
- IoC是一种更广泛的概念,它描述了应用程序的控制权的转移和组件之间的松耦合。
- DI是IoC的一种具体实现方式,它关注如何管理组件之间的依赖关系,以实现IoC的原则。
- 使用DI,依赖关系是通过注入而不是硬编码在组件内部实现的,这使得组件更容易被替换和测试。
- Spring框架是一个典型的IoC容器,它提供了依赖注入的功能,帮助开发者实现IoC原则。
IOC是什么?
IOC(Inversion Of Controll,控制反转,又叫依赖注入)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。
Spring 中的 IoC 的实现原理就是工厂模式加反射机制。
Aop是什么?
AOP(Aspect-Oriented Programming,面向切面编程)AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于增强和扩展应用程序的功能。
能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。
当然也可以使用AspectJ,Spring AOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。使用AOP之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样可以大大简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。日志功能、事务管理和权限管理等场景都用到了AOP。
Aop包含的几个概念
Jointpoint(连接点):具体的切面点点抽象概念,可以是在字段、方法上,Spring中具体表现形式是PointCut(切入点),仅作用在方法上。
Advice(通知): 在连接点进行的具体操作,如何进行增强处理的,分为前置、后置、异常、最终、环绕五种情况。
目标对象:被AOP框架进行增强处理的对象,也被称为被增强的对象。
AOP代理:AOP框架创建的对象,简单的说,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理。
Weaving(织入):将增强处理添加到目标对象中,创建一个被增强的对象的过程
总结为一句话就是:在目标对象(target object)的某些方法(jointpoint)添加不同种类的操作(通知、增强操处理),最后通过某些方法(weaving、织入操作)实现一个新的代理目标对象。
Aop有哪些应用场景
记录日志,权限控制,事务管理,监控性能
有哪些aop的Advice通知类型
特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。
前置通知(Before advice) : 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。
后置通知(After advice) :这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。
返回后通知(After return advice) :这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。
环绕通知(Around advice) :这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。
抛出异常后通知(After throwing advice) :仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。
Aop有哪些实现方式
实现 AOP 的技术,主要分为两大类:
静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;
编译时编织(特殊编译器实现)
类加载时编织(特殊的类加载器实现)。
动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。
JDK 动态代理
JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;
Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新,Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
JDK Proxy 是通过拦截器加反射的方式实现的;
JDK Proxy 只能代理实现接口的类;
JDK Proxy 实现和调用起来比较简单;
CGLIB
CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
CGLib 无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。
106.谈谈对cgLIB的理解
JDK 动态代理机制只能代理实现接口的类,一般没有实现接口的类不能进行代理。使用 CGLib 实现动态代理,完全不受代理类必须实现接口的限制。
CGLib 的原理是对指定目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理。
107.Spring aop和aspectJ aop有什么区别?
Spring AOP是属于运行时增强,而AspectJ是编译时增强。Spring AOP基于代理(Proxying),而AspectJ基于字节码操作(Bytecode Manipulation)。
Spring AOP已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。AspectJ相比于Spring AOP功能更加强大,但是Spring AOP相对来说更简单。
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择AspectJ,它比SpringAOP快很多。
SpringBean的作用域
singleton:唯一bean实例,Spring中的bean默认都是单例的。
prototype:每次请求都会创建一个新的bean实例。
request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。
session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。
Bean的生命周期
- 实例化(Instantiation):在这个阶段,Spring容器创建Bean的实例。这通常是通过构造函数或工厂方法进行的。Bean的定义信息通常在XML配置文件中指定。
- 属性注入(Property Injection):一旦Bean实例化完成,Spring容器会将相关的属性注入到Bean中。这可以通过属性注入、构造函数注入或方法注入(例如
@Autowired
注解)来实现。 - Bean的初始化方法调用(Initialization):在Bean实例化和属性注入之后,容器会调用Bean的初始化方法(如果有定义的话)。可以使用
@PostConstruct
注解或XML配置中的init-method
属性来指定初始化方法。 - Bean的使用(In Use):在Bean初始化完成后,它可以被其他Bean或应用程序组件使用。
- Bean的销毁方法调用(Destruction):在Bean不再需要时,容器可以调用Bean的销毁方法来释放资源。可以使用
@PreDestroy
注解或XML配置中的destroy-method
属性来指定销毁方法。 - Bean的销毁(Disposal):在销毁方法调用后,Bean会被销毁,从Spring容器中移除。这时,Bean的实例将被垃圾回收。
请注意,Bean的生命周期可以受到Spring配置、注解和XML文件的影响。您可以选择使用合适的方法和注解来管理Bean的生命周期,以满足应用程序的需求。
Spring用了哪些设计模式
1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。
2.代理设计模式:Spring AOP功能的实现。
3.单例设计模式:Spring中的bean默认都是单例的。
4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。
5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。
7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。
@Component和@Bean的区别是什么
作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。
将一个类声明为Spring的Bean注解有哪些?
在Spring框架中,你可以使用 @Component
及其相关注解来将一个类声明为Spring的Bean,以便Spring容器能够管理它。以下是一些常用的注解用于将类声明为Spring Bean:
-
@Component:
@Component
是通用的注解,用于声明一个普通的Spring Bean。你可以将它用于任何类,使该类成为一个受Spring容器管理的Bean。@Component public class MyBean { // Bean的代码逻辑 }
-
@Service:
@Service
注解用于表示一个服务层(Service)的Bean。通常用于标识服务层的类,比如业务逻辑处理类。@Service public class MyService { // 服务层的代码逻辑 }
-
@Repository:
@Repository
注解通常用于表示数据访问层(Repository)的Bean,比如数据访问对象(DAO)。@Repository public class MyRepository { // 数据访问层的代码逻辑 }
-
@Controller:
@Controller
注解用于表示控制层(Controller)的Bean,通常用于Spring MVC中的控制器类。@Controller public class MyController { // 控制器层的代码逻辑 }
-
@Configuration:
@Configuration
注解用于表示一个配置类,通常与@Bean
注解一起使用,用于定义Spring Bean。@Configuration public class MyConfig { @Bean public MyBean myBean() { return new MyBean(); } }
这些注解都用于将类声明为Spring Bean,并且被Spring容器扫描后,会自动注册到容器中。你可以根据项目的需要选择合适的注解来声明Bean,并在配置类中进行组件扫描或手动注册。注意,使用这些注解需要确保Spring的组件扫描功能已启用,以便Spring能够自动发现和管理这些Bean。
Spring中自动装配的注解有哪些?
在Spring框架中,有多个注解用于实现自动装配(AutoWiring)依赖关系,使Spring容器能够自动将相应的Bean注入到其他Bean中。以下是一些常用的自动装配注解:
-
@Autowired:
@Autowired
注解是最常用的自动装配注解,它可以用于构造函数、成员变量、方法和参数上。Spring容器会尝试找到匹配类型的Bean并自动注入到被标注的位置。@Autowired private MyBean myBean; @Autowired public MyService(MyDependency dependency) { // 构造函数注入 } @Autowired public void setMyRepository(MyRepository repository) { // Setter方法注入 }
-
@Qualifier:
@Qualifier
注解通常与@Autowired
结合使用,用于指定具体的Bean名称,以解决依赖关系模糊性问题。@Autowired @Qualifier("mySpecificBean") private MyBean myBean;
-
@Resource:
@Resource
注解也用于自动装配,它可以用于字段、setter方法和属性方法上,通常用于按名称进行自动装配。@Resource(name = "mySpecificBean") private MyBean myBean;
-
@Inject:
@Inject
注解是JSR-330规范中定义的,与@Autowired
类似,用于标记需要自动装配的依赖关系。@Inject private MyBean myBean;
这些注解允许你以不同的方式实现自动装配,根据项目需求和个人偏好进行选择。通常情况下,@Autowired
是最常用的自动装配注解,而 @Qualifier
和 @Resource
用于解决装配时的歧义性问题。@Inject
则是一种更通用的自动装配注解,如果项目遵循JSR-330规范,可以使用它。
Spring事务管理的方式有几种
1.编程式事务:在代码中硬编码(不推荐使用)。
2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。
Spring事务中的隔离级别有哪几种
在Spring事务管理中,可以设置不同的事务隔离级别(Isolation Level)来控制多个事务之间的相互影响程度。Spring支持五种标准的事务隔离级别,这些级别与SQL标准中的事务隔离级别相对应。以下是这些事务隔离级别:
-
DEFAULT(默认): 使用底层数据库的默认隔离级别。通常情况下,这等同于数据库的默认级别,例如在MySQL中通常是"REPEATABLE READ"。
-
READ_UNCOMMITTED(读未提交): 允许事务读取其他未提交事务的数据。这是最低的隔离级别,可能导致脏读、不可重复读和幻读等问题。
-
READ_COMMITTED(读已提交): 保证一个事务只能读取已经提交的其他事务的数据,避免了脏读问题。但仍然可能出现不可重复读和幻读问题。
-
REPEATABLE_READ(可重复读): 保证事务执行期间读取的数据保持一致,不受其他事务的影响。这避免了脏读和不可重复读问题,但仍可能出现幻读问题。
-
SERIALIZABLE(串行化): 最高的隔离级别,保证事务串行执行,不会发生并发冲突。这避免了脏读、不可重复读和幻读等所有问题,但性能开销较大,通常不建议在高并发环境中使用。
在Spring中,可以通过在事务注解(如@Transactional
)中设置isolation
属性来指定事务隔离级别。例如:
@Transactional(isolation = Isolation.READ_COMMITTED)
public void myTransactionalMethod() {
// 事务执行逻辑
}
需要根据具体的业务需求和数据一致性要求来选择合适的事务隔离级别。不同的隔离级别在性能和数据一致性之间存在权衡关系,因此需要谨慎选择,并进行充分的测试和评估。
Spring中Bean Factory和ApplicationContext有什么区别?
116.可以通过多少种方式完成依赖注入
构造函数注入
Setter注入
接口注入
。
什么是spring boot
Spring Boot 是 Spring 开源组织下的子项目,是 Spring 组件一站式解决方案,主要是简化了使用 Spring 的难度,简省了繁重的配置,提供了各种启动器,开发者能快速上手。
用来简化Spring应用的初始搭建以及开发过程,使用特定的方式来进行配置
创建独立的Spring引用程序main方法运行
嵌入的tomcat无需部署war文件
简化maven配置
自动配置Spring添加对应的功能starter自动化配置
SpringBoot来简化Spring应用开发,约定大于配置,去繁化简
119.Springboot的原理
为什么使用spring boot(优点)
独立运行
Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。
简化配置
spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。
自动配置
Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 spring
boot-starter-web 启动器就能拥有 web 的功能,无需其他配置。
无代码生成和XML配置
Spring Boot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。
应用监控
Spring Boot 提供一系列端点可以监控服务及应用,做健康检测。
121.请解释一下工厂模式
请解释一下单例模式
懒汉模式:在第一次调用的时候实例
饿汉模式:饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了。
125.消息队列
126.单元测试
@SpringbootTest
@Test
127.zpl语言
ZPL(Zebra Programming Language)是一种专门用于打印标签和条形码的标签打印机编程语言。ZPL是由斑马技术(Zebra Technologies)开发的,并且广泛用于各种标签打印机和条形码打印机上。
ZPL语言的主要目的是控制标签打印机的行为,包括文本、条形码、图像、位置和格式等方面。通过编写ZPL命令,用户可以自定义标签的外观和内容,并将这些命令发送给标签打印机,以在标签上生成所需的标识信息。
以下是一些常见的ZPL语言元素和命令:
^XA // 标签开始
^XZ // 标签结束