开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

请求转发和重定向

发表于 2021-09-20

请求转发

  请求转发,是一种服务器行为,当客户端请求到达后,服务器进行转发,此时会将请求对象进行保存,地址栏中的URL地址不会改变,得到响应后,服务器端再将响应发送给客户端,这个过程只有一次请求发出。

实现方法如下,达到多个资源一起响应的效果。

1
java复制代码request.getRequestDispatcher(url).forward(request,response)
  • 特点:
  1. 服务器只能在当前内部资源进行跳转
  2. 浏览器地址栏不变
  3. 转发只有一次请求
  4. request数据可以共享

接下来通过代码解释下这几个特点。

这是requestDemo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/requestDemo2")
public class requestDemo2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
System.out.println("requestDemo2-->"+username);
request.getRequestDispatcher("/requestDemo3").forward(request,response);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}

这是requestDemo3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/requestDemo3")
public class requestDemo3 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("这是requestDemo3的页面");
String username = request.getParameter("username");
System.out.println("requestDemo3-->"+username);
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}

QQ截图20210920123031.png

  向requestDemo2传入username=zhangsan的参数,接下再看这张图片。
QQ截图20210920123041.png
  Server打印这两条结果,我们发现通过访问requestDemo2的同时也访问到了requestDemo3页面,只是浏览器的地址栏没有改变。

  还有通过浏览器的抓包可以发现,在这次转发的过程中至始至终只有一次请求,服务器只发起了一次请求,但是结果是两个资源都被访问了,所以说转发只有一次请求。

  如果将getRequestDispatcher(url)替换成百度的网址,页面发生跳转就会发生404页面不存在错误,因为百度的网址不是服务器内部的资源。

request作用域

通过request对象可以在一次请求中传递数据,经过请求转发,request域中的数据依然存在,可以通过request传输和共享数据。

1
2
3
4
5
6
java复制代码// 设置域对象内容
request.setAttribute(String name, String value)
// 获取域对象内容
request.getAttribute(String name)
// 删除域对象内容
request.removeAttribute(String name)

重定向

重定向是服务器指导客户端的行为,客户端发出第一个请求,被服务器接收处理后,服务器会作出响应,在响应的同时,服务器会给客户端一个新的资源地址(就是下次请求的地址),当客户端拿到接受到响应后,会马上根据新地址发起第二个请求,服务器接收请求并作出响应,重定向完成。

1
2
3
4
5
6
7
java复制代码/*第一种方法*/
// 设置状态码302
response.setStatus(302);
// 设置响应头location
response.setHeader("location",url);
/*第二种方法*/
response.sendRedirect(url)

废话不多说上代码

这是requestDemo2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/requestDemo2")
public class requestDemo2 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
System.out.println("requestDemo2-->"+username);
response.sendRedirect("/requestDemo3");
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}

这是requestDemo3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/requestDemo3")
public class requestDemo3 extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String username = request.getParameter("username");
System.out.println("requestDemo3-->"+username);
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("demo3....");
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request,response);
}
}

QQ截图20210920155821.png

向requestDemo2传入username=zhangsan的参数,接下再看这张图片。
QQ截图20210920155749.png

  通过浏览器的抓包,可以得出重定向的过程有两次请求,并且requestDemo2的状态码为302,location为/requestDemo3,浏览器的地址栏发生改变。
QQ截图20210920155857.png

  Server的打印结果可以看出,requestDemo3打印zhangsan而requestDemo2却没有,所以重定向不能用request共享数据,因为这是两次请求。重定向还可以访问其他服务器的资源。

总结

请求转发与重定向的比较

请求转发forward 重定向redirect
一次请求 两次请求
数据在request域中共享 request域中数据不共享
浏览器地址栏不改变 浏览器地址栏改变
只能访问当前服务器下的资源 可以访问其他服务器的资源
服务器行为 客户端行为

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring 自带工具类使用学习

发表于 2021-09-20

我们项目大多数都是基于Spring架构,Spring自身包含了许多实用的工具类,学习这些工具类的使用不仅能让我们达到事半功倍的效果,而且还能减少不必要的额外的工具类的引入。查看这些工具类的源码时发现它们都是abstract类型的,这是因为工具类的方法一般都是static静态方法,静态方法和类绑定,类加载后就能使用了,无需实例化(刚好abstract类不能直接实例化,并且可以定义非抽象方法),所以工具类定义为abstract类型再合适不过。

本文print方法为System.out.println的封装:

1
2
3
java复制代码private static void print(Object value) {
System.out.println(value);
}

ClassUtils

org.springframework.util.classUtils包含一些和java.lang.Class相关的实用方法。

getDefaultClassLoader

ClassLoader getDefaultClassLoader()获取当前线程上下文的类加载器:

1
java复制代码print(ClassUtils.getDefaultClassLoader());
1
java复制代码sun.misc.Launcher$AppClassLoader@18b4aac2

overrideThreadContextClassLoader

ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader classLoaderToUse)用特定的类加载器覆盖当前线程上下文的类加载器:

1
2
3
java复制代码print(ClassUtils.getDefaultClassLoader());
ClassUtils.overrideThreadContextClassLoader(ClassLoader.getSystemClassLoader().getParent());
print(ClassUtils.getDefaultClassLoader());
1
2
java复制代码sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3feba861

forName

forName(String name, @Nullable ClassLoader classLoader)通过类名返回类实例,类似于Class.forName(),但功能更强,可以用于原始类型,内部类等:

1
2
3
4
java复制代码ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
print(ClassUtils.forName("int", classLoader));
print(ClassUtils.forName("java.lang.String[]", classLoader));
print(ClassUtils.forName("java.lang.Thread$State", classLoader));
1
2
3
java复制代码int
class [Ljava.lang.String;
class java.lang.Thread$State

isPresent

boolean isPresent(String className, @Nullable ClassLoader classLoader)判断当前classLoader是否包含目标类型(包括它的所有父类和接口):

1
2
3
java复制代码ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
print(ClassUtils.isPresent("int", classLoader));
print(ClassUtils.isPresent("intt", classLoader));
1
2
arduino复制代码true
false

resolvePrimitiveClassName

Class<?> resolvePrimitiveClassName(@Nullable String name)通过给定类名获取原始类:

1
2
java复制代码print(ClassUtils.resolvePrimitiveClassName("int"));
print(ClassUtils.resolvePrimitiveClassName("java.lang.Integer"));
1
2
csharp复制代码int
null

isPrimitiveWrapper

boolean isPrimitiveWrapper(Class<?> clazz)判断给定类是否为包装类,如Boolean, Byte, Character, Short, Integer, Long, Float, Double 或者 Void:

1
2
3
4
java复制代码print(ClassUtils.isPrimitiveWrapper(Integer.class));
print(ClassUtils.isPrimitiveWrapper(Character.class));
print(ClassUtils.isPrimitiveWrapper(Void.class));
print(ClassUtils.isPrimitiveWrapper(String.class));
1
2
3
4
arduino复制代码true
true
true
false

类似的方法还有isPrimitiveOrWrapper判断是否为原始类或者包装类、isPrimitiveWrapperArray判断是否为包装类数组、isPrimitiveArray判断是否为原始类数组。

resolvePrimitiveIfNecessary

Class<?> resolvePrimitiveIfNecessary(Class<?> clazz)如果给定类是原始类,则返回对应包装类,否则直接返回给定类:

1
2
java复制代码print(ClassUtils.resolvePrimitiveIfNecessary(int.class));
print(ClassUtils.resolvePrimitiveIfNecessary(Object.class));
1
2
java复制代码class java.lang.Integer
class java.lang.Object

isAssignable

boolean isAssignable(Class<?> lhsType, Class<?> rhsType)通过反射检查,是否可以将rhsType赋值给lhsType(注意,包装类型可以赋值给相应的原始类型,自动拆装箱机制):

1
2
3
4
5
java复制代码print(ClassUtils.isAssignable(Integer.class, int.class));
print(ClassUtils.isAssignable(Object.class, String.class));
print(ClassUtils.isAssignable(BeanPostProcessor.class, InstantiationAwareBeanPostProcessor.class));
print(ClassUtils.isAssignable(double.class, Double.class)); // consider this
print(ClassUtils.isAssignable(Integer.class, Long.class));
1
2
3
4
5
java复制代码true
true
true
true
false

isAssignableValue

boolean isAssignableValue(Class<?> type, @Nullable Object value)判断给定的值是否符合给定的类型:

1
2
3
4
5
java复制代码print(ClassUtils.isAssignableValue(Integer.class, 1));
print(ClassUtils.isAssignableValue(Integer.class, 1L));
print(ClassUtils.isAssignableValue(int.class, Integer.valueOf(1)));
print(ClassUtils.isAssignableValue(Object.class,1));
print(ClassUtils.isAssignableValue(String.class,1));
1
2
3
4
5
arduino复制代码true
false
true
true
false

convertResourcePathToClassName

String convertResourcePathToClassName(String resourcePath)将类路径转换为全限定类名:

1
java复制代码print(ClassUtils.convertResourcePathToClassName("java/lang/String"));
1
java复制代码java.lang.String

实际上就是将/替换为.。convertClassNameToResourcePath方法功能相反。

classNamesToString

String classNamesToString(Class<?>... classes)直接看演示不解释:

1
java复制代码print(ClassUtils.classNamesToString(String.class, Integer.class, BeanPostProcessor.class));
1
java复制代码[java.lang.String, java.lang.Integer, org.springframework.beans.factory.config.BeanPostProcessor]

getAllInterfaces

Class<?>[] getAllInterfaces(Object instance)返回给定实例对象所实现接口类型集合:

1
2
3
java复制代码AutowiredAnnotationBeanPostProcessor processor = new AutowiredAnnotationBeanPostProcessor();
Class<?>[] allInterfaces = ClassUtils.getAllInterfaces(processor);
Arrays.stream(allInterfaces).forEach(System.out::println);
1
2
3
4
java复制代码interface org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor
interface org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor
interface org.springframework.core.PriorityOrdered
interface org.springframework.beans.factory.BeanFactoryAware

类似的方法还有getAllInterfacesForClass、getAllInterfacesAsSet、getAllInterfacesForClassAsSet

determineCommonAncestor

Class<?> determineCommonAncestor(@Nullable Class<?> clazz1, @Nullable Class<?> clazz2)寻找给定类型的共同祖先(所谓共同祖先指的是给定类型调用class.getSuperclass获得的共同类型,如果给定类型是Object.class,接口,原始类型或者Void,直接返回null):

1
2
3
4
5
6
7
java复制代码// 它两都是接口
print(ClassUtils.determineCommonAncestor(AutowireCapableBeanFactory.class, ListableBeanFactory.class));
print(ClassUtils.determineCommonAncestor(Long.class, Integer.class));
print(ClassUtils.determineCommonAncestor(String.class, Integer.class));
null
class java.lang.Number
null

isInnerClass

boolean isInnerClass(Class<?> clazz)判断给定类型是否为内部类(非静态):

1
2
3
4
5
6
java复制代码class A {
class B {

}
}
print(ClassUtils.isInnerClass(A.B.class)); // true
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码static class A {
class B {

}
}
print(ClassUtils.isInnerClass(A.B.class)); // true
static class A {
static class B {

}
}
print(ClassUtils.isInnerClass(A.B.class)); // false

isCglibProxy

boolean isCglibProxy(Object object)是否为Cglib代理对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@SpringBootApplication
public class AopApplication {

@Configuration
static class MyConfigure {

}

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AopApplication.class, args);
MyConfigure myConfigure = context.getBean(MyConfigure.class);
System.out.println(ClassUtils.isCglibProxy(myConfigure));
}
}
1
arduino复制代码true

配置类不由Cglib代理的话,返回为false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@SpringBootApplication
public class AopApplication {

@Configuration(proxyBeanMethods = false) // 注意这里
static class MyConfigure {

}

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AopApplication.class, args);
MyConfigure myConfigure = context.getBean(MyConfigure.class);
System.out.println(ClassUtils.isCglibProxy(myConfigure));
}
}
1
arduino复制代码false

不过这个方法废弃了,建议使用org.springframework.aop.support.AopUtils.isCglibProxy(Object)方法。

getUserClass

Class<?> getUserClass(Object instance)返回给定实例对应的类型,如果实例是Cglib代理后的对象,则返回代理的目标对象类型:

1
java复制代码print(ClassUtils.getUserClass("Hello")); // class java.lang.String

Cglib代理例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@SpringBootApplication
public class AopApplication {

@Configuration
static class MyConfigure {

}

public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(AopApplication.class, args);
MyConfigure myConfigure = context.getBean(MyConfigure.class);
// 注意它们的区别
System.out.println(myConfigure.getClass());
System.out.println(ClassUtils.getUserClass(myConfigure));
}
}
1
2
java复制代码class cc.mrbird.aop.AopApplication$MyConfigure$$EnhancerBySpringCGLIB$$e51ce45
class cc.mrbird.aop.AopApplication$MyConfigure

matchesTypeName

boolean matchesTypeName(Class<?> clazz, @Nullable String typeName)判断给定class和类型名称是否匹配:

1
java复制代码print(ClassUtils.matchesTypeName(String.class, "java.lang.String")); // true

getShortName

String getShortName(Class<?> clazz)返回类名:

1
java复制代码print(ClassUtils.getShortName(String.class)); // String

getShortNameAsProperty

String getShortNameAsProperty(Class<?> clazz)返回首字母小写的类名,如果是内部类的话,则去掉外部类名:

1
java复制代码print(ClassUtils.getShortNameAsProperty(String.class)); // string
1
2
3
4
5
java复制代码class A {
class B {
}
}
print(ClassUtils.getShortNameAsProperty(String.class)); // b

getClassFileName

String getClassFileName(Class<?> clazz)返回类名+.class:

1
java复制代码print(ClassUtils.getShortNameAsProperty(String.class)); // String.class

getPackageName

String getPackageName(Class<?> clazz)返回包名:

1
java复制代码print(ClassUtils.getShortNameAsProperty(String.class)); // java.lang

getQualifiedName

String getQualifiedName(Class<?> clazz)返回全限定类名,如果是数组类型则末尾加[]:

1
2
java复制代码print(ClassUtils.getQualifiedName(String.class));
print(ClassUtils.getQualifiedName(String[].class));
1
2
java复制代码java.lang.String
java.lang.String[]

getQualifiedMethodName

String getQualifiedMethodName(Method method)获取方法的全限定名:

1
2
3
java复制代码print(ClassUtils.getQualifiedMethodName(
ClassUtils.class.getDeclaredMethod("getQualifiedMethodName", Method.class
)));
1
java复制代码org.springframework.util.ClassUtils.getQualifiedMethodName

hasConstructor

boolean hasConstructor(Class<?> clazz, Class<?>... paramTypes)判断给定类型是否有给定类型参数构造器:

1
2
java复制代码print(ClassUtils.hasConstructor(String.class, String.class));
print(ClassUtils.hasConstructor(String.class, Object.class));
1
2
arduino复制代码true
false

getConstructorIfAvailable

<T> Constructor<T> getConstructorIfAvailable(Class<T> clazz, Class<?>... paramTypes)返回给定类型的给定参数类型构造器,没有的话返回null:

1
2
3
java复制代码Constructor<String> constructorIfAvailable = ClassUtils.getConstructorIfAvailable(String.class, String.class);
print(constructorIfAvailable != null);
print(constructorIfAvailable.toString());
1
2
java复制代码true
public java.lang.String(java.lang.String)

hasMethod

boolean hasMethod(Class<?> clazz, Method method)判断给定类型是否有指定的方法:

1
2
java复制代码Method hasMethod = ClassUtils.class.getDeclaredMethod("hasMethod", Class.class, Method.class);
print(ClassUtils.hasMethod(ClassUtils.class, hasMethod)); // true

重载方法boolean hasMethod(Class<?> clazz, String methodName, Class<?>... paramTypes)。

getMethod

Method getMethod(Class<?> clazz, String methodName, @Nullable Class<?>... paramTypes)从指定类型中找指定方法,没找到抛IllegalStateException异常:

1
java复制代码ClassUtils.getMethod(ClassUtils.class,"hello", String.class);
1
java复制代码java.lang.IllegalStateException: Expected method not found: java.lang.NoSuchMethodException: org.springframework.util.ClassUtils.hello(java.lang.String)

如果希望没找到返回null,而非抛异常,可以用getMethodIfAvailable方法。

getMethodCountForName

int getMethodCountForName(Class<?> clazz, String methodName)从指定类型中通过方法名称查找该方法个数(重写、重载、非public的都算):

1
java复制代码print(ClassUtils.getMethodCountForName(ClassUtils.class,"hasMethod")); // 2

类似的方法还有hasAtLeastOneMethodWithName,至少得有一个。

getStaticMethod

Method getStaticMethod(Class<?> clazz, String methodName, Class<?>... args)获取给定类型的静态方法,如果该方法不是静态的或者没有这个方法,则返回null:

1
2
3
java复制代码Method method = ClassUtils.getStaticMethod(ClassUtils.class, "getDefaultClassLoader");
print(method != null);
print(method.getReturnType());
1
2
java复制代码true
class java.lang.ClassLoader

FileSystemUtils

文件系统实用工具类

deleteRecursively

boolean deleteRecursively(@Nullable File root)递归删除指定文件或目录,删除成功返回true,失败返回false,不会抛出异常。

实用File的delete目录尝试删除a目录:

1
2
java复制代码File file = new File("a");
print(file.delete()); // false

因为a目录包含子目录(文件),所以应该使用递归删除:

1
2
java复制代码File file = new File("a");
print(FileSystemUtils.deleteRecursively(file)); // true

重载方法boolean deleteRecursively(@Nullable Path root)和该方法功能相似,但该方法可能会抛出IO异常。

copyRecursively

void copyRecursively(File src, File dest)递归复制src文件到dest(目标路径不存在则自动创建):

1
2
3
java复制代码File src = new File("a");
File dest = new File("aa");
FileSystemUtils.copyRecursively(src, dest);

重载方法void copyRecursively(Path src, Path dest)。

StreamUtils

包含一些文件流的实用方法默认的缓冲区大小为4096bytes。

注意:该工具类的所有方法都不会对流进行关闭处理!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

万字Java并发编程面试题(含答案,收藏版)

发表于 2021-09-20

大家好,我是大彬。最近在面试,看了很多面经,抽空将Java并发编程常见的面试题总结了一下,在这里分享给大家~

文章目录:

image.png
image.png

给大家分享一个github仓库,上面放了200多本经典的计算机书籍,包括C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~

github.com/Tyson0314/j…

如果github访问不了,可以访问gitee仓库。

gitee.com/tysondai/ja…

线程池

线程池:一个管理线程的池子。

为什么使用线程池?

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。统一管理线程,避免系统创建大量同类线程而导致消耗完内存。
1
arduino复制代码public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);

线程池执行原理?

创建新的线程需要获取全局锁,通过这种设计可以尽量避免获取全局锁,当 ThreadPoolExecutor 完成预热之后(当前运行的线程数大于等于 corePoolSize),提交的大部分任务都会被放到 BlockingQueue。

为了形象描述线程池执行,打个比喻:

  • 核心线程比作公司正式员工
  • 非核心线程比作外包员工
  • 阻塞队列比作需求池
  • 提交任务比作提需求

线程池参数有哪些?

ThreadPoolExecutor 的通用构造函数:

1
arduino复制代码public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
  • corePoolSize:当有新任务时,如果线程池中线程数没有达到线程池的基本大小,则会创建新的线程执行任务,否则将任务放入阻塞队列。当线程池中存活的线程数总是大于 corePoolSize 时,应该考虑调大 corePoolSize。
  • maximumPoolSize:当阻塞队列填满时,如果线程池中线程数没有超过最大线程数,则会创建新的线程运行任务。否则根据拒绝策略处理新任务。非核心线程类似于临时借来的资源,这些线程在空闲时间超过 keepAliveTime 之后,就应该退出,避免资源浪费。
  • BlockingQueue:存储等待运行的任务。
  • keepAliveTime:非核心线程空闲后,保持存活的时间,此参数只对非核心线程有效。设置为0,表示多余的空闲线程会被立即终止。
  • TimeUnit:时间单位
1
2
3
4
5
6
7
java复制代码TimeUnit.DAYS
TimeUnit.HOURS
TimeUnit.MINUTES
TimeUnit.SECONDS
TimeUnit.MILLISECONDS
TimeUnit.MICROSECONDS
TimeUnit.NANOSECONDS
  • ThreadFactory:每当线程池创建一个新的线程时,都是通过线程工厂方法来完成的。在 ThreadFactory 中只定义了一个方法 newThread,每当线程池需要创建新线程就会调用它。
1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyThreadFactory implements ThreadFactory {
private final String poolName;

public MyThreadFactory(String poolName) {
this.poolName = poolName;
}

public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);//将线程池名字传递给构造函数,用于区分不同线程池的线程
}
}
  • RejectedExecutionHandler:当队列和线程池都满了时,根据拒绝策略处理新任务。
1
2
3
4
java复制代码AbortPolicy:默认的策略,直接抛出RejectedExecutionException
DiscardPolicy:不处理,直接丢弃
DiscardOldestPolicy:将等待队列队首的任务丢弃,并执行当前任务
CallerRunsPolicy:由调用线程处理该任务

线程池大小怎么设置?

如果线程池线程数量太小,当有大量请求需要处理,系统响应比较慢影响体验,甚至会出现任务队列大量堆积任务导致OOM。

如果线程池线程数量过大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换(cpu给线程分配时间片,当线程的cpu时间片用完后保存状态,以便下次继续运行),从 而增加线程的执行时间,影响了整体执行效率。

CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止某些原因导致的任务暂停(线程阻塞,如io操作,等待锁,线程sleep)而带来的影响。一旦某个线程被阻塞,释放了cpu资源,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。

I/O 密集型任务(2N): 系统会用大部分的时间来处理 I/O 操作,而线程等待 I/O 操作会被阻塞,释放 cpu资源,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法:最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时)),一般可设置为2N

线程池的类型有哪些?适用场景?

常见的线程池有 FixedThreadPool、SingleThreadExecutor、CachedThreadPool 和 ScheduledThreadPool。这几个都是 ExecutorService (线程池)实例。

FixedThreadPool

固定线程数的线程池。任何时间点,最多只有 nThreads 个线程处于活动状态执行任务。

1
2
3
arduino复制代码public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用无界队列 LinkedBlockingQueue(队列容量为 Integer.MAX_VALUE),运行中的线程池不会拒绝任务,即不会调用RejectedExecutionHandler.rejectedExecution()方法。

maxThreadPoolSize 是无效参数,故将它的值设置为与 coreThreadPoolSize 一致。

keepAliveTime 也是无效参数,设置为0L,因为此线程池里所有线程都是核心线程,核心线程不会被回收(除非设置了executor.allowCoreThreadTimeOut(true))。

适用场景:适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。需要注意的是,FixedThreadPool 不会拒绝任务,在任务比较多的时候会导致 OOM。

SingleThreadExecutor

只有一个线程的线程池。

1
2
3
csharp复制代码public static ExecutionService newSingleThreadExecutor() {
return new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}

使用无界队列 LinkedBlockingQueue。线程池只有一个运行的线程,新来的任务放入工作队列,线程处理完任务就循环从队列里获取任务执行。保证顺序的执行各个任务。

适用场景:适用于串行执行任务的场景,一个任务一个任务地执行。在任务比较多的时候也是会导致 OOM。

CachedThreadPool

根据需要创建新线程的线程池。

1
2
3
csharp复制代码public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}

如果主线程提交任务的速度高于线程处理任务的速度时,CachedThreadPool 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。

使用没有容量的SynchronousQueue作为线程池工作队列,当线程池有空闲线程时,SynchronousQueue.offer(Runnable task)提交的任务会被空闲线程处理,否则会创建新的线程处理任务。

适用场景:用于并发执行大量短期的小任务。CachedThreadPool允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

ScheduledThreadPoolExecutor

在给定的延迟后运行任务,或者定期执行任务。在实际项目中基本不会被用到,因为有其他方案选择比如quartz。

使用的任务队列 DelayQueue 封装了一个 PriorityQueue,PriorityQueue 会对队列中的任务进行排序,时间早的任务先被执行(即ScheduledFutureTask 的 time 变量小的先执行),如果time相同则先提交的任务会被先执行(ScheduledFutureTask 的 squenceNumber 变量小的先执行)。

执行周期任务步骤:

  1. 线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期任务是指 ScheduledFutureTask的 time 大于等于当前系统的时间;
  2. 执行这个 ScheduledFutureTask;
  3. 修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时间;
  4. 把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

适用场景:周期性执行任务的场景,需要限制线程数量的场景。

进程线程

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。
线程是比进程更小的执行单位,它是在一个进程中独立的控制流,一个进程可以启动多个线程,每条线程并行执行不同的任务。

线程的生命周期

初始(NEW):线程被构建,还没有调用 start()。

运行(RUNNABLE):包括操作系统的就绪和运行两种状态。

阻塞(BLOCKED):一般是被动的,在抢占资源中得不到资源,被动的挂起在内存,等待资源释放将其唤醒。线程被阻塞会释放CPU,不释放内存。

等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。

超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。

终止(TERMINATED):表示该线程已经执行完毕。

图片来源:Java并发编程的艺术

讲一下线程中断?

线程中断即线程运行过程中被其他线程给打断了,它与 stop 最大的区别是:stop 是由系统强制终止线程,而线程中断则是给目标线程发送一个中断信号,如果目标线程没有接收线程中断的信号并结束线程,线程则不会终止,具体是否退出或者执行其他逻辑取决于目标线程。

线程中断三个重要的方法:

1、java.lang.Thread#interrupt

调用目标线程的interrupt()方法,给目标线程发一个中断信号,线程被打上中断标记。

2、java.lang.Thread#isInterrupted()

判断目标线程是否被中断,不会清除中断标记。

3、java.lang.Thread#interrupted

判断目标线程是否被中断,会清除中断标记。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码private static void test2() {
Thread thread = new Thread(() -> {
while (true) {
Thread.yield();

// 响应中断
if (Thread.currentThread().isInterrupted()) {
System.out.println("Java技术栈线程被中断,程序退出。");
return;
}
}
});
thread.start();
thread.interrupt();
}

创建线程有哪几种方式?

  • 通过扩展Thread类来创建多线程
  • 通过实现Runnable接口来创建多线程,可实现线程间的资源共享
  • 实现Callable接口,通过FutureTask接口创建线程。
  • 使用Executor框架来创建线程池。

继承 Thread 创建线程代码如下。run()方法是由jvm创建完操作系统级线程后回调的方法,不可以手动调用,手动调用相当于调用普通方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码/**
* @author: 程序员大彬
* @time: 2021-09-11 10:15
*/
public class MyThread extends Thread {
public MyThread() {
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread() + ":" + i);
}
}

public static void main(String[] args) {
MyThread mThread1 = new MyThread();
MyThread mThread2 = new MyThread();
MyThread myThread3 = new MyThread();
mThread1.start();
mThread2.start();
myThread3.start();
}
}

Runnable 创建线程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* @author: 程序员大彬
* @time: 2021-09-11 10:04
*/
public class RunnableTest {
public static void main(String[] args){
Runnable1 r = new Runnable1();
Thread thread = new Thread(r);
thread.start();
System.out.println("主线程:["+Thread.currentThread().getName()+"]");
}
}

class Runnable1 implements Runnable{
@Override
public void run() {
System.out.println("当前线程:"+Thread.currentThread().getName());
}
}

实现Runnable接口比继承Thread类所具有的优势:

  1. 资源共享,适合多个相同的程序代码的线程去处理同一个资源
  2. 可以避免java中的单继承的限制
  3. 线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类

Callable 创建线程代码:

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
java复制代码/**
* @author: 程序员大彬
* @time: 2021-09-11 10:21
*/
public class CallableTest {
public static void main(String[] args) {
Callable1 c = new Callable1();

//异步计算的结果
FutureTask<Integer> result = new FutureTask<>(c);

new Thread(result).start();

try {
//等待任务完成,返回结果
int sum = result.get();
System.out.println(sum);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}

}

class Callable1 implements Callable<Integer> {

@Override
public Integer call() throws Exception {
int sum = 0;

for (int i = 0; i <= 100; i++) {
sum += i;
}
return sum;
}
}

使用 Executor 创建线程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* @author: 程序员大彬
* @time: 2021-09-11 10:44
*/
public class ExecutorsTest {
public static void main(String[] args) {
//获取ExecutorService实例,生产禁用,需要手动创建线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//提交任务
executorService.submit(new RunnableDemo());
}
}

class RunnableDemo implements Runnable {
@Override
public void run() {
System.out.println("大彬");
}
}

什么是线程死锁?

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

下面通过例子说明线程死锁,代码来自并发编程之美。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2

public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();

new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}

代码输出如下:

1
2
3
4
java复制代码Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。

线程死锁怎么产生?怎么避免?

死锁产生的四个必要条件:

  • 互斥:一个资源每次只能被一个进程使用(资源独立)
  • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放(不释放锁)
  • 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺(抢夺资源)
  • 循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭(死循环)

避免死锁的方法:

  • 第一个条件 “互斥” 是不能破坏的,因为加锁就是为了保证互斥
  • 一次性申请所有的资源,破坏 “占有且等待” 条件
  • 占有部分资源的线程进一步申请其他资源时,如果申请不到,主动释放它占有的资源,破坏 “不可抢占” 条件
  • 按序申请资源,破坏 “循环等待” 条件

线程run和start的区别?

调用 start() 方法是用来启动线程的,轮到该线程执行时,会自动调用 run();直接调用 run() 方法,无法达到启动多线程的目的,相当于主线程线性执行 Thread 对象的 run() 方法。
一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang.IllegalThreadStateException 异常;run() 方法没有限制。

线程都有哪些方法?

join

Thread.join(),在main中创建了thread线程,在main中调用了thread.join()/thread.join(long millis),main线程放弃cpu控制权,线程进入WAITING/TIMED_WAITING状态,等到thread线程执行完才继续执行main线程。

1
2
3
java复制代码public final void join() throws InterruptedException {
join(0);
}

yield

Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的CPU时间片,但不释放锁资源,由运行状态变为就绪状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。该方法与sleep()类似,只是不能由用户指定暂停多长时间。

1
java复制代码public static native void yield(); //static方法

sleep

Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入TIMED_WAITING状态,让出cpu资源,但不释放对象锁,指定时间到后又恢复运行。作用:给其它线程执行机会的最佳方式。

1
java复制代码public static native void sleep(long millis) throws InterruptedException;//static方法

volatile底层原理

volatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。

  1. 当对volatile变量进行写操作的时候,JVM会向处理器发送一条LOCK前缀的指令,将该变量所在缓存行的数据写回系统内存。
  2. 由于缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存中。

MESI(缓存一致性协议):当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,就会从内存重新读取。

volatile关键字的两个作用:

  1. 保证了不同线程对共享变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。

指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。Java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止处理器重排序。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。对一个volatile字段进行写操作,Java内存模型将在写操作后插入一个写屏障指令,这个指令会把之前的写入值都刷新到内存。

AQS原理

AQS,AbstractQueuedSynchronizer,抽象队列同步器,定义了一套多线程访问共享资源的同步器框架,许多并发工具的实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch。

AQS使用一个volatile的int类型的成员变量state来表示同步状态,通过CAS修改同步状态的值。当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1。如果 state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。

1
java复制代码private volatile int state;//共享变量,使用volatile修饰保证线程可见性

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态(独占或共享 )构造成为一个节点(Node)并将其加入同步队列并进行自旋,当同步状态释放时,会把首节中的后继节点对应的线程唤醒,使其再次尝试获取同步状态。

synchronized的用法有哪些?

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

Synchronized的作用有哪些?

原子性:确保线程互斥的访问同步代码;
可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的 “对一个变量unlock 操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”。

synchronized 底层实现原理?

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在 执行 monitorexit 指令后,将锁计数器设为0
,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

ReentrantLock 是如何实现可重入性的?

ReentrantLock 内部自定义了同步器 Sync,在加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,检查当前维护的那个线程 ID 和当前请求的线程 ID 是否 一致,如果一致,同步状态加1,表示锁被当前线程获取了多次。

ReentrantLock和synchronized区别

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。

wait()和sleep()的区别

相同点:

  1. 使当前线程暂停运行,把机会交给其他线程
  2. 任何线程在等待期间被中断都会抛出InterruptedException

不同点:

  1. wait() 是Object超类中的方法;而sleep()是线程Thread类中的方法
  2. 对锁的持有不同,wait()会释放锁,而sleep()并不释放锁
  3. 唤醒方法不完全相同,wait() 依靠notify或者notifyAll 、中断、达到指定时间来唤醒;而sleep()到达指定时间被唤醒
  4. 调用obj.wait()需要先获取对象的锁,而 Thread.sleep()不用

wait(),notify()和suspend(),resume()之间的区别

  • wait() 使得线程进入阻塞等待状态,并且释放锁
  • notify()唤醒一个处于等待状态的线程,它一般跟wait()方法配套使用。
  • suspend()使得线程进入阻塞状态,并且不会自动恢复,必须对应的resume() 被调用,才能使得线程重新进入可执行状态。suspend()方法很容易引起死锁问题。
  • resume()方法跟suspend()方法配套使用。

suspend()不建议使用,suspend()方法在调用后,线程不会释放已经占有的资 源(比如锁),而是占有着资源进入睡眠状态,这样容易引发死锁问题。

Runnable和 Callable有什么区别?

  • Callable接口方法是call(),Runnable的方法是run();
  • Callable接口call方法有返回值,支持泛型,Runnable接口run方法无返回值。
  • Callable接口call()方法允许抛出异常;而Runnable接口run()方法不能继续上抛异常;

volatile和synchronized的区别是什么?

  1. volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。
  2. volatile至保证可见性;synchronized保证原子性与可见性。
  3. volatile禁用指令重排序;synchronized不会。
  4. volatile不会造成阻塞;synchronized会。

线程执行顺序怎么控制?

假设有T1、T2、T3三个线程,你怎样保证T2在T1执行完后执行,T3在T2执行完后执行?

可以使用join方法解决这个问题。比如在线程A中,调用线程B的join方法表示的意思就是**:A等待B线程执行完毕后(释放CPU执行权),在继续执行。**

代码如下:

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
csharp复制代码public class ThreadTest {

public static void main(String[] args) {

Thread spring = new Thread(new SeasonThreadTask("春天"));
Thread summer = new Thread(new SeasonThreadTask("夏天"));
Thread autumn = new Thread(new SeasonThreadTask("秋天"));

try
{
//春天线程先启动
spring.start();
//主线程等待线程spring执行完,再往下执行
spring.join();
//夏天线程再启动
summer.start();
//主线程等待线程summer执行完,再往下执行
summer.join();
//秋天线程最后启动
autumn.start();
//主线程等待线程autumn执行完,再往下执行
autumn.join();
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

class SeasonThreadTask implements Runnable{

private String name;

public SeasonThreadTask(String name){
this.name = name;
}

@Override
public void run() {
for (int i = 1; i <4; i++) {
System.out.println(this.name + "来了: " + i + "次");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果:

1
2
3
4
5
6
7
8
9
makefile复制代码春天来了: 1次
春天来了: 2次
春天来了: 3次
夏天来了: 1次
夏天来了: 2次
夏天来了: 3次
秋天来了: 1次
秋天来了: 2次
秋天来了: 3次

乐观锁一定就是好的吗?

乐观锁避免了悲观锁独占对象的现象,提高了并发性能,但它也有缺点:

  • 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
  • 长时间自旋可能导致开销大。假如 CAS 长时间不成功而一直自旋,会 给 CPU 带来很大的开销。
  • ABA 问题。CAS 的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是 A, 后来被一条线程改为 B,最后又被改成了 A,则 CAS 认为此内存值并 没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。

守护线程是什么?

守护线程是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些 发生的事件。在 Java 中垃圾回收线程就是特殊的守护线程。

线程间通信方式

volatile

volatile是轻量级的同步机制,volatile保证变量对所有线程的可见性,不保证原子性。

synchronized

保证线程对变量访问的可见性和排他性。

等待通知机制

wait/notify为 Object 对象的方法,调用wait/notify需要先获得对象的锁。对象调用wait之后线程释放锁,将线程放到对象的等待队列,当通知线程调用此对象的notify()方法后,等待线程并不会立即从wait返回,需要等待通知线程释放锁(通知线程执行完同步代码块),等待队列里的线程获取锁,获取锁成功才能从wait()方法返回,即从wait方法返回前提是线程获得锁。

等待通知机制依托于同步机制,目的是确保等待线程从wait方法返回时能感知到通知线程对对象的变量值的修改。

ThreadLocal

线程本地变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

ThreadLocal原理

每个线程都有一个ThreadLocalMap(ThreadLocal内部类),Map中元素的键为ThreadLocal,而值对应线程的变量副本。

调用threadLocal.set()–>调用getMap(Thread)–>返回当前线程的ThreadLocalMap<ThreadLocal, value>–>map.set(this, value),this是ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

调用get()–>调用getMap(Thread)–>返回当前线程的ThreadLocalMap<ThreadLocal, value>–>map.getEntry(this),返回value

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码    public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

threadLocals的类型ThreadLocalMap的键为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,如longLocal和stringLocal。

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
csharp复制代码public class ThreadLocalDemo {
ThreadLocal<Long> longLocal = new ThreadLocal<>();

public void set() {
longLocal.set(Thread.currentThread().getId());
}
public Long get() {
return longLocal.get();
}

public static void main(String[] args) throws InterruptedException {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());

Thread thread = new Thread(() -> {
threadLocalDemo.set();
System.out.println(threadLocalDemo.get());
}
);

thread.start();
thread.join();

System.out.println(threadLocalDemo.get());
}
}

ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。

ThreadLocal内存泄漏的原因?

每个Thread都有⼀个ThreadLocalMap的内部属性,map的key是ThreaLocal,定义为弱引用,value是强引用类型。GC的时候会⾃动回收key,而value的回收取决于Thread对象的生命周期。一般会通过线程池的方式复用Thread对象节省资源,这也就导致了Thread对象的生命周期比较长,这样便一直存在一条强引用链的关系:Thread –> ThreadLocalMap–>Entry–>Value,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。

解决⽅法:每次使⽤完ThreadLocal就调⽤它的remove()⽅法,手动将对应的键值对删除,从⽽避免内存泄漏。

1
2
3
4
java复制代码currentTime.set(System.currentTimeMillis());
result = joinPoint.proceed();
Log log = new Log("INFO",System.currentTimeMillis() - currentTime.get());
currentTime.remove();

ThreadLocal使用场景有哪些?

ThreadLocal 适用场景:每个线程需要有自己单独的实例,且需要在多个方法中共享实例,即同时满足实例在线程间的隔离与方法间的共享。比如Java web应用中,每个线程有自己单独的 Session 实例,就可以使用ThreadLocal来实现。

锁的分类

公平锁与非公平锁

按照线程访问顺序获取对象锁。synchronized 是非公平锁, Lock 默认是非公平锁,可以设置为公平锁,公平锁会影响性能。

1
2
3
4
5
6
7
java复制代码public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}

共享式与独占式锁

共享式与独占式的最主要区别在于:同一时刻独占式只能有一个线程获取同步状态,而共享式在同一时刻可以有多个线程获取同步状态。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

悲观锁与乐观锁

悲观锁,每次访问资源都会加锁,执行完同步代码释放锁,synchronized 和 ReentrantLock 属于悲观锁。

乐观锁,不会锁定资源,所有的线程都能访问并修改同一个资源,如果没有冲突就修改成功并退出,否则就会继续循环尝试。乐观锁最常见的实现就是CAS。

乐观锁一般来说有以下2种方式:

  1. 使用数据版本记录机制实现,这是乐观锁最常用的一种实现方式。给数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的version字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
  2. 使用时间戳。数据库表增加一个字段,字段类型使用时间戳(timestamp),和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

适用场景:

  • 悲观锁适合写操作多的场景。
  • 乐观锁适合读操作多的场景,不加锁可以提升读操作的性能。

CAS

什么是CAS?

CAS全称 Compare And Swap,比较与交换,是乐观锁的主要实现方式。CAS 在不使用锁的情况下实现多线程之间的变量同步。ReentrantLock 内部的 AQS 和原子类内部都使用了 CAS。

CAS算法涉及到三个操作数:

  • 需要读写的内存值 V。
  • 进行比较的值 A。
  • 要写入的新值 B。

只有当 V 的值等于 A 时,才会使用原子方式用新值B来更新V的值,否则会继续重试直到成功更新值。

以 AtomicInteger 为例,AtomicInteger 的 getAndIncrement()方法底层就是CAS实现,关键代码是 compareAndSwapInt(obj, offset, expect, update),其含义就是,如果obj内的value和expect相等,就证明没有其他线程改变过这个变量,那么就更新它为update,如果不相等,那就会继续重试直到成功更新值。

CAS存在的问题?

CAS 三大问题:

  1. ABA问题。CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。但是如果内存值原来是A,后来变成了B,然后又变成了A,那么CAS进行检查时会发现值没有发生变化,但是实际上是有变化的。ABA问题的解决思路就是在变量前面添加版本号,每次变量更新的时候都把版本号加一,这样变化过程就从A-B-A变成了1A-2B-3A。

JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,原子更新带有版本号的引用类型。
2. 循环时间长开销大。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
3. 只能保证一个共享变量的原子操作。对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的。

Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

并发工具

在JDK的并发包里提供了几个非常有用的并发工具类。CountDownLatch、CyclicBarrier和Semaphore工具类提供了一种并发流程控制的手段。

CountDownLatch

CountDownLatch用于某个线程等待其他线程执行完任务再执行,与thread.join()功能类似。常见的应用场景是开启多个线程同时执行某个任务,等到所有任务执行完再执行特定操作,如汇总统计结果。

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
java复制代码public class CountDownLatchDemo {
static final int N = 4;
static CountDownLatch latch = new CountDownLatch(N);

public static void main(String[] args) throws InterruptedException {

for(int i = 0; i < N; i++) {
new Thread(new Thread1()).start();
}

latch.await(1000, TimeUnit.MILLISECONDS); //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行;等待timeout时间后count值还没变为0的话就会继续执行
System.out.println("task finished");
}

static class Thread1 implements Runnable {

@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + "starts working");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}
}
}

运行结果:

1
2
3
4
5
java复制代码Thread-0starts working
Thread-1starts working
Thread-2starts working
Thread-3starts working
task finished

CyclicBarrier

CyclicBarrier(同步屏障),用于一组线程互相等待到某个状态,然后这组线程再同时执行。

1
2
3
4
java复制代码public CyclicBarrier(int parties, Runnable barrierAction) {
}
public CyclicBarrier(int parties) {
}

参数parties指让多少个线程或者任务等待至某个状态;参数barrierAction为当这些线程都达到某个状态时会执行的内容。

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
java复制代码public class CyclicBarrierTest {
// 请求的数量
private static final int threadCount = 10;
// 需要同步的线程数量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);

for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}

public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
/**等待60秒,保证子线程完全执行结束*/
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}

}

运行结果如下,可以看出CyclicBarrier是可以重用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码threadnum:0is ready
threadnum:1is ready
threadnum:2is ready
threadnum:3is ready
threadnum:4is ready
threadnum:4is finish
threadnum:3is finish
threadnum:2is finish
threadnum:1is finish
threadnum:0is finish
threadnum:5is ready
threadnum:6is ready
...

当四个线程都到达barrier状态后,会从四个线程中选择一个线程去执行Runnable。

CyclicBarrier和CountDownLatch区别

CyclicBarrier 和 CountDownLatch 都能够实现线程之间的等待。

CountDownLatch用于某个线程等待其他线程执行完任务再执行。CyclicBarrier用于一组线程互相等待到某个状态,然后这组线程再同时执行。
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可用于处理更为复杂的业务场景。

Semaphore

Semaphore类似于锁,它用于控制同时访问特定资源的线程数量,控制并发线程数。

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
java复制代码public class SemaphoreDemo {
public static void main(String[] args) {
final int N = 7;
Semaphore s = new Semaphore(3);
for(int i = 0; i < N; i++) {
new Worker(s, i).start();
}
}

static class Worker extends Thread {
private Semaphore s;
private int num;
public Worker(Semaphore s, int num) {
this.s = s;
this.num = num;
}

@Override
public void run() {
try {
s.acquire();
System.out.println("worker" + num + " using the machine");
Thread.sleep(1000);
System.out.println("worker" + num + " finished the task");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

运行结果如下,可以看出并非按照线程访问顺序获取资源的锁,即

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码worker0 using the machine
worker1 using the machine
worker2 using the machine
worker2 finished the task
worker0 finished the task
worker3 using the machine
worker4 using the machine
worker1 finished the task
worker6 using the machine
worker4 finished the task
worker3 finished the task
worker6 finished the task
worker5 using the machine
worker5 finished the task

原子类

基本类型原子类

使用原子的方式更新基本类型

  • AtomicInteger:整型原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean :布尔型原子类

AtomicInteger 类常用的方法:

1
2
3
4
5
6
7
java复制代码public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

AtomicInteger 类主要利用 CAS (compare and swap) 保证原子操作,从而避免加锁的高开销。

数组类型原子类

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray :引用类型数组原子类

AtomicIntegerArray 类常用方法:

1
2
3
4
5
6
7
java复制代码public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

引用类型原子类

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:带有版本号的引用类型原子类。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
  • AtomicMarkableReference :原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来

本文已经收录到github仓库,此仓库用于分享Java相关知识总结,包括Java基础、MySQL、Spring Boot、MyBatis、Redis、RabbitMQ、计算机网络、数据结构与算法等等,欢迎大家提pr和star!

github.com/Tyson0314/J…

如果github访问不了,可以访问gitee仓库。

gitee.com/tysondai/Ja…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Dubbo 30 Reference 扫描流程

发表于 2021-09-19

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 .前言

前面几篇看了 Dobbo 的注册流程 , 这一阶段看看Dubbo 的发现流程

Dubbo 的发现包括以下几个环节 :

  • Reference 的扫描和代理
  • Reference 服务发现
  • 负载均衡
  • 服务降级

二 . Reference 获取

访问入口

1
2
3
4
5
6
7
8
9
10
11
java复制代码public DubboBootstrap start() {

//..省略注册和容器初始化

// Reference 扫描的起点为 DubboBootstrap , 其中有这样一段代码
// 发起 Refer Services 处理
referServices();

//.........

}

2.1 初始化逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码private void referServices() {
if (cache == null) {
cache = ReferenceConfigCache.getCache();
}
// <dubbo:reference sticky="false" id="demoService" /> -> 2.2 列表获取
configManager.getReferences().forEach(rc -> {
// TODO, compatible with ReferenceConfig.refer()
ReferenceConfig<?> referenceConfig = (ReferenceConfig<?>) rc;
referenceConfig.setBootstrap(this);
if (!referenceConfig.isRefreshed()) {
referenceConfig.refresh();
}

if (rc.shouldInit()) {
if (rc.shouldReferAsync()) {
ExecutorService executor = executorRepository.getExportReferExecutor();
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
try {
cache.get(rc);
} catch (Throwable t) {
logger.error("refer async catch error : " + t.getMessage(), t);
}
}, executor);

asyncReferringFutures.add(future);
} else {
cache.get(rc);
}
}
});
}

2.2 References 管理

列表获取

1
2
3
4
5
6
java复制代码// 2.1 里面可以看到从缓存中获取 , 以下看一下获取的流程
configManager.getReferences()

public Collection<ReferenceConfigBase<?>> getReferences() {
return getConfigs(getTagName(ReferenceConfigBase.class));
}

看到这里基本上就能回忆起来了 , 上文这个地方就已经见过了, 其主要原理是获取 Config 前缀 ,然后从一个集合中获取对应的配置信息 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public static String getTagName(Class<?> cls) {
String tag = cls.getSimpleName();
for (String suffix : SUFFIXES) {
if (tag.endsWith(suffix)) {
tag = tag.substring(0, tag.length() - suffix.length());
break;
}
}
return StringUtils.camelToSplitName(tag, "-");
}


final Map<String, Map<String, AbstractConfig>> configsCache = newMap();
// configType -> reference
private <C extends AbstractConfig> Map<String, C> getConfigsMap(String configType) {
return (Map<String, C>) read(() -> configsCache.getOrDefault(configType, emptyMap()));
}

再来看一下那个图 :

image.png

三 . 扫描生成 Reference

3.1 扫描的入口

接下来看看Reference 是怎么扫描生成的 , 和上文看到的一样 , 一切的起点是 InitializingBean # afterPropertiesSet 触发的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码C- ReferenceBean
public void afterPropertiesSet() throws Exception {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();

// 获取当前 Bean 的 BeanDefinition
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(getId());
this.interfaceClass = (Class<?>) beanDefinition.getAttribute(ReferenceAttributes.INTERFACE_CLASS);
this.interfaceName = (String) beanDefinition.getAttribute(ReferenceAttributes.INTERFACE_NAME);

// 获取 Reference 属性
if (beanDefinition.hasAttribute(Constants.REFERENCE_PROPS)) {
referenceProps = (Map<String, Object>) beanDefinition.getAttribute(Constants.REFERENCE_PROPS);
} else {
// 省略其他图解的属性解析
}

// 获取 ReferenceBeanManager 并且发起注册逻辑
ReferenceBeanManager referenceBeanManager = beanFactory.getBean(ReferenceBeanManager.BEAN_NAME, ReferenceBeanManager.class);
referenceBeanManager.addReference(this);
}

3.2 RefenceBean 管理入口

下面来看一下 Reference 的只要流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public void addReference(ReferenceBean referenceBean) throws Exception {

String referenceBeanName = referenceBean.getId();
PropertyResolver propertyResolver = applicationContext.getEnvironment();

// 校验缓存中是否存在 ,且是否为同一对象
String referenceKey = ReferenceBeanSupport.generateReferenceKey(referenceBean, propertyResolver);
ReferenceBean oldReferenceBean = referenceIdMap.get(referenceBeanName);

// 省略oldRefence 校验逻辑 , 该逻辑要求如果缓存中存在 , 则其应用地址应该一致
// PS : 因为使用的的 != 比较的地址 , 避免重新重复的 Reference 对象

referenceIdMap.put(referenceBeanName, referenceBean);
// 保存缓存,将引用键映射到referencebename
this.registerReferenceKeyAndBeanName(referenceKey, referenceBeanName);

// 如果在 prepareReferenceBeans()之后添加引用,应该立即初始化它
if (initialized) {
initReferenceBean(referenceBean);
}
}

3.3 RefenceBean 初始化

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
java复制代码private synchronized void  initReferenceBean(ReferenceBean referenceBean) throws Exception {

if (referenceBean.getReferenceConfig() != null) {
return;
}

// 首先构建 referenceKey
String referenceKey = ReferenceBeanSupport.generateReferenceKey(referenceBean, applicationContext.getEnvironment());

// 从缓存中获取 ReferenceConfig
// PS : 看过 Dubbo 层级图可以看到 , 这是第二层 Config 层最核心的2个类之一
ReferenceConfig referenceConfig = referenceConfigMap.get(referenceKey);
if (referenceConfig == null) {

//创建一个真实的 ReferenceConfig
Map<String, Object> referenceAttributes = ReferenceBeanSupport.getReferenceAttributes(referenceBean);
referenceConfig = ReferenceCreator.create(referenceAttributes, applicationContext)
.defaultInterfaceClass(referenceBean.getObjectType())
.build();

// set id if it is not a generated name
if (referenceBean.getId() != null && !referenceBean.getId().contains("#")) {
referenceConfig.setId(referenceBean.getId());
}

// Step 1 : 缓存 : cache referenceConfig
referenceConfigMap.put(referenceKey, referenceConfig);

// Step 2 : 注册 : register ReferenceConfig
// 核心 : 调用逻辑添加 Config 到集合中
DubboBootstrap.getInstance().reference(referenceConfig);
}

// Step 3 : 关联 associate referenceConfig to referenceBean
referenceBean.setKeyAndReferenceConfig(referenceKey, referenceConfig);
}

3.4 添加到集合中

1
2
3
4
5
6
java复制代码// C- DubboBootstrap
public DubboBootstrap reference(ReferenceConfig<?> referenceConfig) {
referenceConfig.setBootstrap(this);
configManager.addReference(referenceConfig);
return this;
}

核心添加流程

1
2
3
4
5
6
7
JAVA复制代码// PS : 中秋耶看过这个逻辑 , 获取集合并且添加
protected <T extends AbstractConfig> T addConfig(AbstractConfig config, boolean unique) {
return (T) write(() -> {
Map<String, AbstractConfig> configsMap = configsCache.computeIfAbsent(getTagName(config.getClass()), type -> newMap());
return addIfAbsent(config, configsMap, unique);
});
}

3.5 ReferenceBeanManager 补充

ReferenceBeanManager 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码// 注意 , 这里实现了 ApplicationContextAware , 以完成容器的通知操作
public class ReferenceBeanManager implements ApplicationContextAware {
//reference bean id/name -> ReferenceBean
private Map<String, ReferenceBean> referenceIdMap = new ConcurrentHashMap<>();

//reference key -> [ reference bean names ]
private Map<String, List<String>> referenceKeyMap = new ConcurrentHashMap<>();

//reference key -> ReferenceConfig instance
private Map<String, ReferenceConfig> referenceConfigMap = new ConcurrentHashMap<>();

private ApplicationContext applicationContext;

}

补充

3.3 创建 ReferenceConfig 流程

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
java复制代码
referenceConfig = ReferenceCreator.create(referenceAttributes, applicationContext)
.defaultInterfaceClass(referenceBean.getObjectType())
.build();

// Step 1 : 准备 ReferenceCreator 对象
public static ReferenceCreator create(Map<String, Object> attributes, ApplicationContext applicationContext) {
return new ReferenceCreator(attributes, applicationContext);
}

// Step 2 : 创建 ReferenceConfig
public final ReferenceConfig build() throws Exception {
ReferenceConfig configBean = new ReferenceConfig();
// 核心配置
configureBean(configBean);
return configBean;

}

// Step 3 : 核心配置流程
protected void configureBean(ReferenceConfig configBean) throws Exception {
populateBean(attributes, configBean);
configureMonitorConfig(configBean);
configureApplicationConfig(configBean);
configureModuleConfig(configBean);
configureConsumerConfig(attributes, configBean);
}

// PS : 这一段比较多 , 后面单独看看

整体流程图

image.png
image.png

ReferenceConfig 数据

1
java复制代码<dubbo:reference id="demoService" />

image.png

总结

Reference 比较有看头的就是 Spring 的深入使用 , 这个点值得深入学习一下

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

NFT 这么火,你知道 ERC721 么

发表于 2021-09-19

如果对币圈稍微有些关注的人,这几个月一定被 NFT 刷屏了。面对一张张卖出天价的 NFT,绝大多数人都无法理解,其实对于 NFT,贵的不是那张图,而是大家的共识。

这篇文章暂时不讨论 NFT 的价值问题,而是来起底一下 NFT 的技术支撑。

目前绝大多数的 NFT 资产都在以太坊发行,占了主流资产的 90% 以上。目前 NFT 的发行标准最主流的是 ERC721,这已经成为事实上的标准。

ERC 是什么

在开始说 ERC721 之前,需要先说明一下 ERC,以太坊还在不断的发展,包括协议和各类标准,就像互联网行业的 RFC 一样,每个人都可以都对以太坊的发展提出自己的意见。每个意见都被称之为 EIP(Ethereum Improvement Proposals),直接在这个 GitHub 仓库(github.com/ethereum/EI…)中提交。

EIP 的完整流程如下:

  • Draft:由作者提交的建议,还在做主要的修改
  • Review:建议已经基本完成,可以进行 EIP 评审
  • Last Call:评审完成,这个建议有可能成为最终版
  • Accepted:这个建议在等待以太坊开发者的实现或者部署
  • Final:EIP 会成为一个以太坊的标准

大多数的 EIP 都会止步于 Review 阶段。

ERC(Ethereum Request for Comment) 用于记录以太坊上的各种开发标准和协议,部分 EIP 会成为 ERC。 ERC 都是 EIP,反之则不对。

ERC 721 也是通过这个流程提出来的。

ERC 721 的全称是非同质化代币标准(Non-Fungible Token Standard)。以太币和在以太坊网络上流通的一些代币称之为同质化代币。这些同质化代币发行的标准是 ERC 20 标准。如果代币不实现这些标准,那么就无法在以太坊网络中流通。

从流通的角度来说,ERC 721 和 ERC 20 的实现都是以太坊网络中的代币。它们最大的区别在于,ERC 721 的每一个代币都是独一无二的,有着自己的属性,相互之间是不等价的。而 ERC 20 的每一枚代币都是相同的,是等价的。

ERC 721 协议详解

ERC 721 其实是定义了一系列的接口,如果写过 Java 的人会发现,这个接口的形式与 Java 的非常类似。下面接口定义使用的以太坊的智能合约语言 Solidity:

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
jsx复制代码pragma solidity ^0.4.20;

interface ERC721 /* is ERC165 */ {

event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);

event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);

event ApprovalForAll(address indexed _owner, address indexed _operator, bool _approved);

function balanceOf(address _owner) external view returns (uint256);

function ownerOf(uint256 _tokenId) external view returns (address);

function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;

function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;

function transferFrom(address _from, address _to, uint256 _tokenId) external payable;

function approve(address _approved, uint256 _tokenId) external payable;

function setApprovalForAll(address _operator, bool _approved) external;

function getApproved(uint256 _tokenId) external view returns (address);

function isApprovedForAll(address _owner, address _operator) external view returns (bool);
}

interface ERC165 {
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}

interface ERC721TokenReceiver {

function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4);
}

interface ERC721Metadata /* is ERC721 */ {

function name() external view returns (string _name);

function symbol() external view returns (string _symbol);

function tokenURI(uint256 _tokenId) external view returns (string);
}

interface ERC721Enumerable /* is ERC721 */ {

function totalSupply() external view returns (uint256);

function tokenByIndex(uint256 _index) external view returns (uint256);

function tokenOfOwnerByIndex(address _owner, uint256 _index) external view returns (uint256);
}

balanceOf 方法用来判断一个地址下有多少个 NFT,ownerOf 方法用来判断一个 NFT 是不是属于一个地址。transferFrom 和 safeTransferFrom 都是用于 NFT 的转账,但是 safeTransferFrom 在把 NFT 转到零地址时会报错。零地址就是以太坊中的黑洞,任何转入其中的资产都无法取出。

approve 类的地址就是把自己的 NFT 托管给其他管理,这点要注意,千万随意在陌生的网站上执行力 approve 操作,特别是 setApprovalForAll 方法,否则自己的 NFT 别人就尅随意操控了。

如果要你要发行自己的 NFT,使用 solidity 实现之后,发布到以太坊上,就发行成功了。在 ERC 721 中,推荐把上面的所有接口都实现。

在 ERC 721 中,基本包含了 ERC 20 的所有的接口,非同质化代币虽然是独一无二的,但也需要能够转账等代币的基础特性。

相比于 ERC 20,ERC 721 最大的不同是 有了 ERC721Metadata 这个接口,这个接口可以用来标识每一个非同质化代币的属性,也就是 NFT 的元数据。每个非同质化代币的具体属性都通过 tokenURI 这个方法来返回。我们在 OpenSea 上看到的那些 NFT,都是通过调用这个方法获取 NFT 的详情。

实现 ERC721 时,必须实现 ERC165 协议,这个用来检测当前的合约的是否实现了某个接口,在以太坊中,每一个 interface 都有自己的 interfaceId,比如 ERC165 的是 0x01ffc9a7,ERC 721 的是 0x80ac58cd,ERC721Metadata 的是 0x780e9d63,ERC721Enumerable 的是 0x5b5e139f。

ERC721TokenReceiver 用于在调用 transfer 方法之后的回调,如果传的值对不上,就会导致这次 transfer 失败。

ERC721Enumerable 接口中则实现了 NFT 的一些不变的属性,比如总供应量,通过代币的序号来获取 NFT,列举某个地址下的所有 NFT。

ERC 721 实例

一起来看一下 Meebits 的智能合约,合约的地址可以在 etherscan.io/address/0x7… 这里看到。这就是以太坊上应用的神奇之处,所有的代码都是公开的。

Meebits 是 Larva Labs 发行的一组 NFT,总数2 万个。

代码的总行数只有 679 行,逻辑都还是比较简单的。比如我们上面说到标识 NFT 元数据的 tokenURI 方法,Meebits 是这样实现的:

1
2
3
jsx复制代码function tokenURI(uint256 _tokenId) external view validNFToken(_tokenId) returns (string memory) {
return string(abi.encodePacked("https://meebits.larvalabs.com/meebit/", toString(_tokenId)));
}

实际上是指向了另外一个地址,我我们在这个 url 后面随机输入一个编号,就可以得到一个 Meebits 的元数据:

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
jsx复制代码// https://meebits.larvalabs.com/meebit/1
{
"name": "Meebit #1",
"description": "Meebit #1",
"image": "http://meebits.larvalabs.com/meebitimages/characterimage?index\u003d1\u0026type\u003dfull\u0026imageType\u003djpg",
"attributes": [
{
"trait_type": "Type",
"value": "Human"
},
{
"trait_type": "Hair Style",
"value": "Bald"
},
{
"trait_type": "Hat",
"value": "Backwards Cap"
},
{
"trait_type": "Hat Color",
"value": "Gray"
},
{
"trait_type": "Shirt",
"value": "Skull Tee"
},
{
"trait_type": "Overshirt",
"value": "Athletic Jacket"
},
{
"trait_type": "Overshirt Color",
"value": "Red"
},
{
"trait_type": "Pants",
"value": "Cargo Pants"
},
{
"trait_type": "Pants Color",
"value": "Camo"
},
{
"trait_type": "Shoes",
"value": "Workboots"
}
]
}

这里我们重点来看几个上面的没有讲到的地方,上面的 ERC 721 中的方法是我们全都都要实现的,但是只实现上面的那些代码却不够,因为在一个 NFT 项目中,发行出来之后, NFT 是要拿出去售卖的,而不是靠 transfer 来手动转出。meebits 公开售卖的方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
jsx复制代码function mint() external payable reentrancyGuard returns (uint) {
require(publicSale, "Sale not started.");
require(!marketPaused);
require(numSales < SALE_LIMIT, "Sale limit reached.");
uint salePrice = getPrice();
require(msg.value >= salePrice, "Insufficient funds to purchase.");
if (msg.value > salePrice) {
msg.sender.transfer(msg.value.sub(salePrice));
}
beneficiary.transfer(salePrice);
numSales++;
return _mint(msg.sender, 0);
}

在这个方法中,限制了开售的时间和每次购买的个数,如果还未开售,或者超过了限购次数就会购买失败。当然,如果你账户中的钱不够,也会购买失败。购买收到的钱都会转入到当前的合约账户下。

售卖 NFT 的钱自然不能永远放在合约账户中,而是要提出来。所以还需要一个提款的方法:

1
2
3
4
5
6
7
jsx复制代码function withdraw(uint amount) external reentrancyGuard {
require(amount <= ethBalance[msg.sender]);
ethBalance[msg.sender] = ethBalance[msg.sender].sub(amount);
(bool success, ) = msg.sender.call{value:amount}("");
require(success);
emit Withdraw(msg.sender, amount);
}

这个方法也很简单,也许有人会有疑问,这个方法谁都可以调用,那岂不是谁都可以来把钱提走了其实并不是,如果是当前合约之外的账号过来提钱,那么都只能提到 0,因为 ethBalance 数组中并没有其他账号的钱,所以只能是当前合约账号才能把钱提出来,这点设计也很巧妙。

ERC 721 不是终点

ERC 721 虽然现在很受欢迎,但满足不了所有的场景。比如在游戏场景中,很多的装备一次可能会发行多个,多达几千种装备类型,如果为每种装备发行一个 NFT,那就要发行几千个合约,这个代价很大,也会对以太坊的资源造成很大的浪费。

ERC 1155 的目标就是解决这个问题,可以在一个合约中定义多种 NFT,也可以为一种 NFT 定义多个数量,后续我们再详细展开 ERC 1155。

文 / Rayjun

[1] eips.ethereum.org/EIPS/eip-20

[2] eips.ethereum.org/EIPS/eip-72…

[3] eips.ethereum.org/EIPS/eip-11…

[4]eips.ethereum.org/EIPS/eip-16…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

linux配置上网 配置上网:

发表于 2021-09-19

配置上网:

配置网卡。来实现上网。我们以linux虚拟机为例。步骤如下:

看一下本机linux有没有配置ip:

图片2.png
之后我们确认linux配置完ip之后,我们在配在vmware这款软件上,配置我们需要的虚拟网卡。

image.png
编辑—》虚拟网络编辑器

image.png
点击添加网络

image.png
选择要添加的虚拟网卡(这里以vmnet9为例子)

image.png
选择nat模式(注意其他网卡已经有nat模式时就要把那个网卡的nat选项取消掉)

把dhcp去掉

子网IP设置为与虚拟系统一致网段,这里以我的虚拟机地址为例(192.168.4.7)所以子网设置为192.168.4.0 之后我们点击nat设置

image.png
之后会弹出另一个窗口,网关ip设置192.168.4.2或者192.168.4.254.–》一路确定

image.png
确定好之后我们进行配置真实机IP。

image.png

之后出现图片如下(点击更改适配器)

image.png

我们找到我们新添加的vmware9

image.png
右键—-》属性–》ipv4–》配置相应地址(地址规则虚拟机为同一网段)

image.png

我们回到,虚拟机,右键虚拟机–》设置

image.png

image.png
点击网络适配器,选择我们添加的网卡vmnet9.–》确定

image.png
然后我们测试连通性。(ok通了)

image.png

我们把我们之前的网关配置上。

1
2
3
4
5
markdown复制代码命令模板:

nmcli connection modify 配置的虚拟机网卡名字 ipv4.method manual ipv4.dns 8.8.8.8 ipv4.gateway 上边步骤配置的网关 connection.autoconnect yes

nmcli connection up 虚拟机网卡名字

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mysql事务隔离级别与锁机制

发表于 2021-09-19

写在前面

前面复习总结了Mysql索引的相关知识内容。接下来来总结一些mysql的事务和锁机制。在开发使用过程中,mysql都会有并发操作,并发执行多个事务对相同的一批数据进行增删改查,会导致脏读、脏写、不可重复读、幻读问题。为了解决多事务之间的并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,来解决多事务的并发问题。首先来了解一下事务及ACID。

事务和ACID

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,叫ACID。

  • 原子性(Atomicity):事务是一个原子操作,对数据的操作,要么全部执行,要么全部不执行。
  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。
  • 隔离性(Isolation):数据库系统提供了隔离机制,不同事务之间的操作,互不影响。
  • 持久性(Durable):事务完成之后,它对于数据的修改是永久性的。

并发事务处理带来的问题

更新丢失(脏写)

当多个事务在对同一行数据更新改行时,由于事务之间相互隔离,不知道其他事务的存在,就会发生脏写问题,最后的更新覆盖了其他事务所做的更新。(白话:第一个事务读取到一行数据值为3,第二个事务读取到相同行数据也为3,第一个事务先更新数据值3+1=4,第二个事务,也更新数据值3+2=5,此时第二个事务的值5会覆盖第一个事务更新的值。正确情况是:第一个事务更新3+1=4,第二个事务更新要在第一个事务的基础上,4+2=6才对)

脏读

一个事务正在对一条记录做修改,事务完成,但还没提交,这条记录就处于不一致的状态,这时候另一个事务来读取相同一条记录,如果不加控制,第二个事务读取了这些”脏”数据(也就是数据已经被修改,但未提交的数据),并据此作进一步的处理,就会产生未提交的数据依赖关系。(白话:事务A读取到了事务B修改但未提交的数据,还在这个数据的基础上做了操作,如果事务B回滚,A读取的数据无效,不符合一致性要求)

不可重复读

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,发现数据已经被修改或者删除。(白话:事务A内部相同的查询语句在不同时刻查询出来的结果不一致)

幻读

一个事务按相同的查询条件读取以前读取过的记录,发现其他事务插入了满足其查询条件的新数据。(白话:事务A读取到了事务B提交的新增数据,不符合隔离性)

事务隔离级别

隔离级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可串行化 不可能 不可能 不可能

数据库的事务隔离级别越严格,并发副作用越小,但付出的代价越高,因为事务隔离实质上就是使事务在一定程度上“串行化”。

查看当前数据库事务隔离级别: show variables like’tx_isolation’;

设置事务隔离级别:set tx_isolation=’REPEATABLE-READ’;
Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用MySQL设置的隔离级别。

锁

  • 从性能上分为乐观锁和悲观锁
  • 从对数据库的操作类型分为读锁和写锁(都属于悲观锁)
    读锁(共享锁,S锁):针对同一份数据,多个读操作可以同时进行而不会互相影响

写锁(排他锁,X锁):当前在写操作,会阻塞其他写锁和读锁

  • 从对数据操作的粒度分为表锁和行锁
    表锁每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁的粒度大,发送锁冲突的概率高,并发低;一般用在表数据迁移的场景。
    行锁每次操作锁住一行数据。开销大,加锁慢,会出现死锁,锁的粒度小,发生锁冲突的概率低,并发度高。

行锁与事务隔离级别示例

读未提交

1.打开客户端A,设置set tx_isolation=’read-uncommitted’;(读未提交),查询表account的值。
image.png

2.在客户端A,事务提交之前,打开客户端B,更新表account的值。
image.png

3.在客户端A查询account表的数据,发现可以查询到客户端B未提交的事务;

image.png

4.客户端B,进行回滚,所有的操作都会被取消,那客户端A查询到数据就变成了脏数据

image.png

5.在客户端A执行更新语句update account set balance = balance - 50 where id =1,没有变成350,跟预想的结果不一致。这是因为在更新数据库的时候使用了balance = balance - 50,balance是数据库里面最新的数据,所以不会变成350,但是如果在应用程序代码中,一般是先查出来balance的数值,然后再减去50,也就是查出来的值是400,这时候并应用程序并不知道,MySQL的事务回滚了,再减去50,这时候就变成了350。 也就是脏读。

image.png

读已提交

1.打开客户端A设置set tx_isolation=’read-committed’; 读已提交,查询表中记录。
image.png

2.在客户端A事务提交之前打开B客户端,更新记录。
image.png

3.客户端B事务还没提交,客户端A不能查询到B已经更新了的数据,解决了脏读问题
image.png

4.客户端B事务提交。
image.png

5.客户端A执行的查询结果,与上一次不一致,产生了不可重复读问题。
image.png

可重复读

1.客户端A,设置set tx_isolation=’repeatable-read’; 可重复读,查询记录。
image.png

2.客户端B,更新并提交记录。
image.png

3.在客户端A,同样查询,结果一致,解决了不可重复读问题。

image.png

4.在客户端A,继续执行update account set balance = balance - 50 where id = 1更新操作,balance没有变成400-50=350,变成了300,数据的一致性并没有被破坏。在可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读;insert、update、delete会更新版本号,是当前读。

image.png

5.客户端B,重新开启一个事务。并插入数据

image.png

6.在客户端A,查询记录,没有出现新增的数据,没有出现幻读。
image.png

间隙锁

间隙锁,锁的是两个值之间空隙。MySQL的默认隔离级别是可重复读

image.png

例如:上图间隙有id为(3,10),(10,20),(20,正无穷)三个区间,执行update account set name=’smart’ where id >7 and id <18;则其他session就没有办法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任务数据。即id在(3,20]区间都无法修改数据。间隙锁是在可重复读隔离级别下才会生效

临键锁

临键锁是行锁和间隙锁的组合。例如上面(3,20]的整个区间可叫做临键锁

无索引行锁会升级为表锁

锁优化建议

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽量减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
  • 尽可能低级别事务隔离

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【源码分析】kafka是如何解决粘包拆包的?

发表于 2021-09-19

​

kafka版本:0.9.0

前面笔者写了一篇文章一文讲清粘包拆包 全面的讲解了TCP粘包拆包相关的问题。下面进行一个简单的总结:

TCP粘包拆包产生的原因是,应用层有意义的数据包,传输层的协议并不了解其含义,不会去根据你的业务内容去分包和发送,只会按照自己的协议格式传送数据。

知道问题的本质后,解决问题就简单了。就需要在应用层收到数据后根据标识判断一下,数据是否完整,如果完整了我们再进行数据包解析,最后交给业务代码处理。

解决粘包拆包问题的方法:

(1)消息定长;

(2)增加特殊字符进行分割,比如每条数据末尾都添加一个换行符;

(3)自定义协议,例如 len + data,其中len是代表data的字节长度;

kakfa是如何解决粘包拆包问题的呢?

首先看粘包,也就是接收到了多余的数据,该如何拆分数据包,读取到正确完整的数据包?

kafka使用到是上面的第三种解决方法,自定义协议格式。

kafka接收到数据包后,会进行这些操作:

  1. 先读取前4字节,转换为一个int,即长度;
  2. 根据长度申请内存buffer;
  3. 最后读取指定大小的数据到申请好的buffer中;

具体代码实现在:KafkaChannel.read()

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
arduino复制代码public NetworkReceive read() throws IOException {
NetworkReceive result = null;
if (receive == null) {
receive = new NetworkReceive(maxReceiveSize, id);
}
// 读取数据
receive(receive);
// 判断是否读取完数据
if (receive.complete()) {
// 数据读取完后,rewind一下,准备读
receive.payload().rewind();
result = receive;
receive = null;
}
//
return result;
}

private long receive(NetworkReceive receive) throws IOException {
return receive.readFrom(transportLayer);
}

public long readFrom(ScatteringByteChannel channel) throws IOException {
return readFromReadableChannel(channel);
}

public long readFromReadableChannel(ReadableByteChannel channel) throws IOException {
int read = 0;
// size就是存放len的缓冲区,大小4个字节,定义:size = ByteBuffer.allocate(4);
if (size.hasRemaining()) {
int bytesRead = channel.read(size);
if (bytesRead < 0)
throw new EOFException();
read += bytesRead;
// 读完了长度
if (!size.hasRemaining()) {
// rewind一下,准备获取具体的len值
size.rewind();
// size里的值就是接下来要读取的数据的长度
int receiveSize = size.getInt();
if (receiveSize < 0)
throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + ")");
if (maxSize != UNLIMITED && receiveSize > maxSize)
throw new InvalidReceiveException("Invalid receive (size = " + receiveSize + " larger than " + maxSize + ")");
// 分配读取数据的缓冲区
this.buffer = ByteBuffer.allocate(receiveSize);
}
}
if (buffer != null) {
int bytesRead = channel.read(buffer);
if (bytesRead < 0)
throw new EOFException();
read += bytesRead;
}
// 返回读取的总长度
return read;
}

接下来,再看看拆包代码。拆包也就是接收到数据不够组成一条完整的数据,该如何等待完整的数据包

最主要的代码,在上面的receive.complete()方法中的判断逻辑。​​​​​​​

1
2
3
arduino复制代码public boolean complete() {
return !size.hasRemaining() && !buffer.hasRemaining();
}
  • !size.hasRemaining():接收到的len数据已经读取完成;
  • !buffer.hasRemaining():接收到的data数据已经读取完成;

两个条件同时成立,也就是说既要读取完len,也要读取完data,才算读取了完整的一条数据。

只要一条数据没读完整,那么receive.complete()函数返回值就是false,那么最终返回的结果就是null,等待下一次OP_READ事件的时候再接着上次没读完的数据读取,直到读取一条完整的数据为止。

那么这次读取的数据就会暂存起来,存入stageReceives这个数据结构中等待下一次读取。​​​​​​​

1
2
3
4
5
6
7
scss复制代码if (channel.ready() && key.isReadable() && !hasStagedReceive(channel)) {
NetworkReceive networkReceive;
while ((networkReceive = channel.read()) != null)
addToStagedReceives(channel, networkReceive);
}

官方对stageReceives的解释:

END

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用Import注册bean到Spring容器中 1 导

发表于 2021-09-18

我们知道注册bean到IOC中有很多中方式,比如xml方式 ,JavaConfig方式 包括:(@Compent,@Service,@Controller,@Repository,@Bean)等等。
但是除了以上几种,有没有其他方式把我们的对象交给IOC让他去管理呢?

答案肯定是有啦!!! 那就是@Import();

@Import支持三种类型的导入。

  1. 普通类
  2. 配置类(如 Configuration修饰的类)
  3. 实现了 ImportSelector(可以选择性导入到IOC)` 接口
  4. 实现了 ImportBeanDefinitionRegistrar 接口 (可以添加bean定义信息到spring中(使用RootBeanDefinition的registerBeanDefinition方法),从而被spring创建bean并管理)

下边我们分别举例并测试一下~

  1. 导入普通类到IOC容器

我们来试一把 (注意 ImportJavaConfigList中的@Configuration被我注掉了)
image.png

哎嗨报错了。加上@Configuration我们再看看

image.png

ok没问题,普通的类EmailService可以注册到spring容器中了。


  1. 导入配置类(Configuration修饰的类)到IOC容器

image.png

如图,可以看到RedisTemplateExample 被 spring 管理,有人说我不@Import也行呀,spring也会扫描到RedisConfigurationExample配置类从而创建bean呀,这里我想说的是,其实我觉得@Import中的参数即xxx.class更多是在非springboot启动类项目中的配置类,举个例子: 假如我有个admin服务(是个springboot项目),然后admin需要依赖common的pom文件,common是非springboot应用,这个时候,其实你可以指定扫描路径扫描到common中的xxx.class配置类(比如@scan("com.xzll.*")),也可以使用这种@Import的方式导入该xxx.class。

  1. 导入实现了ImportSelector接口的类到IOC容器

  • 这里模拟一个场景就是(根据注解值的不同,来使用不同的bean来进行报警消息发送)
3.1 定义一个注解 EnableAlarmNotice
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* @Author: hzz
* @Date: 2021/9/18 16:33:34
* @Description: 加载springboot启动类上 用于指定开启哪些报警方式
*/
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import(AlarmServiceSelector.class)
public @interface EnableAlarmNotice {
String[] types() default {"ding_ding"};
}
3.2 自定义个选择器 (根据上边注解的types获取不同的bean) 实现 ImportSelector
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
java复制代码/**
* @Author: hzz
* @Date: 2021/9/18 17:01:08
* @Description: 报警方式选择器 根据EnableAlarmNotice注解中的types字段来进行选择性注入bean
*/
public class AlarmServiceSelector implements ImportSelector {

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {

//获取注解上的type,然后根据types数组选择性的创建bean 放到IOC中
Map<String, Object> map = annotationMetadata.getAnnotationAttributes(EnableAlarmNotice.class.getName(), true);
List<String> needInIOCBean = new ArrayList<>();
if (map != null && !map.isEmpty()) {
String[] types = (String[]) map.get("types");
if (Objects.isNull(types) || types.length == 0)
return new String[0];

for (int i = 0; i < types.length; i++) {
needInIOCBean.add(AlarmNoticeTypeEnum.getClassFullName(types[i]));
}
}
return needInIOCBean.toArray(new String[0]);
}
}
3.3 搞个枚举 来定义报警的方式以及bean的全类名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum AlarmNoticeTypeEnum {

EMAIL("email", "com.xzll.common.alarm.service.impl.EmailAlarmNoticeImpl"),
DING_DING("ding_ding", "com.xzll.common.alarm.service.impl.DingDingAlarmNoticeImpl");

private String type;
private String classFullName;

public static String getClassFullName(String type) {
AlarmNoticeTypeEnum[] var1 = values();
int var2 = var1.length;

for (int var3 = 0; var3 < var2; ++var3) {
AlarmNoticeTypeEnum alarmNoticeTypeEnum = var1[var3];
if (Objects.equals(alarmNoticeTypeEnum.type, type)) {
return alarmNoticeTypeEnum.getClassFullName();
}
}
return StringUtils.EMPTY;
}
}
3.4 在启动类开启注解并设置ding_ding和email两个值
1
2
3
4
5
6
7
8
9
java复制代码@SpringBootTest(classes = StudyTestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
@RunWith(SpringJUnit4ClassRunner.class)
@EnableRabbitMq
@EnableAlarmNotice( types= {"ding_ding",
"email"})
@Slf4j
public class StudyTestApplicationTest {

}
3.5 执行单元测试看下结果

image.png

但是我发现 用@Service注册的bean和实现ImportSelector注册的bean的id不一样,这个在实际中要注意下
如下所示:

image.png

ok关于 ImportSelector就说到这


  1. 导入实现了ImportBeanDefinitionRegistrar 接口的类到IOC容器

4.1 我们实现ImportBeanDefinitionRegistrar接口,并重写其方法,然后再该方法中,可以对bean信息进行修改或者增加。

如下所示:

image.png

ok关于@Import注解今天就说这些了,有时间整理一篇其源码文章。相信会对这个注解有更深刻的理解。大后天中秋节,祝大家中秋快乐!!!


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

谁说 java 不能搞炫酷的月亮🌛

发表于 2021-09-18

我正在参加中秋创意投稿大赛,详情请看:中秋创意投稿大赛

思路

  1. 输出一个月亮
  2. 清屏
  3. 换一个颜色输出一个月亮
  4. 清屏
  5. 换一个颜色输出一个月亮
  6. 达到效果

难点

java 如何清空控制台????

代码

一、清空控制台

  • 思路
+ 在c++ 中 `window:system("cls");` `mac/linux:system("clear");`可以直接清空
+ 可以通过 JNI 实现 java 调用 C++/C 的dll
+ 声明一个 native
+ 将class通过javah编译成 xx.h
+ 编写c++项目
+ 生成 dll
+ 将 dll copy到自己的jdk\jre\lib的目录下
+ java代码调用
  • 声明一个 native
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Cls {

/**
* 声明本地方法
*/
public native void clear();

static {
//加载动态链接库DLL
//必须保证加载的 xxxx.dll 或者 xxx.dylib 在你jdk\jre\lib下目录
System.loadLibrary("cls");
}
}
  • 将class通过javah编译成 xx.h

image.png

  • 编写c++项目

image.png

image.png

  • 生成 dll

ctrl/command + F9

  • 将 dll copy到 自己的 jdk\jre\lib 的目录下
  • java 调用

image.png

  • 效果

Kapture 2021-09-18 at 12.40.31.gif

二、绘制月亮

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
java复制代码public class zhongqiu {
public static void main(String[] args) throws Exception {
for (int i = 31; i <= 36; i++) {
//清空控制台
new Cls().clear();
//输出月亮
yueLiang(i);
//慢一点,不然会闪
Thread.sleep(300);
//循环换颜色
if (i == 36) {
i = 31;
}
}
}

//输出月亮
public static void yueLiang(int c) {
int r = 15;
double R = 2 * r;
for (int i = 0; i <= R; i += 2) {
double y = Math.round(Math.sqrt(r * r - Math.pow(r - i, 2)));
double x = r - y;
int b = i == 0 || R - i == 0 ? 4 : 0;
for (int j = 0; j < x + 2 * y + b; j++) {
System.out.print(" ");
}
int yl = 6;
if (i == 0 || R - i == 0) {
yl = 2;
} else if (i == 1 || R - i == 1) {
yl = 4;
} else if (i == 2 || R - i == 2) {
yl = 5;
}
for (int j = 0; j < yl; j++) {
System.out.print("\033[" + c + "m*\033[m");
}
System.out.println("");
}
}

}

效果

Kapture 2021-09-18 at 13.27.53.gif

注意事项:不能在idea运行,要去终端运行才有效果

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…525526527…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%