基本概念
操作系统中 heap 和 stack 的区别
heap是堆,stack是栈,是两种不同的数据结构。堆是队列优先,先进先出;栈是先进后出。
在java多线程中,每个线程都有自己的栈;不同的线程共享一个堆。
在java内存中,栈中存放的大多数是方法的参数、局部变量,调用完后立即释放空间;堆中存放的是由new创建的对象和数组,生命周期由JVM的垃圾回收算法决定。
什么是基于注解的切面实现
首先说切面编程:为了方便,将一些公共的类似的地方抽取出来,开发时只需要关注具体业务,这个公共类似的东西就是通知(Advice,包括安全、事物、日志等),可以使用通知的地方称为连接点(JoinPoint,如每个方法的前后、抛出异常的地方),
使用了通知的连接点为切入点(Pointcut),通知和切入点结合就是切面(Aspect),引入(introduction)和使用切面的类为目标(target),使用的是代理(proxy)来实现整套AOP机制。
一开始配置切入点是在spring的xml文件中进行的,之后可以在目标类中使用注解实现,spring在启动时会加载和扫描包含注解之处,然后再实现动态代理。使用到的注解包括:@Aspect、@Pointcut、@Before、@After、@Component等。
demo可以参考这里:https://github.com/aJavaBird/demo.spring.aspect
什么是 对象/关系 映射集成模块
对象关系映射(Object Relational Mapping,简称ORM)是通过描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到数据库中。甚至开发人员都不用管数据库的存储,只需知道如何存对象即可。已有的ORM框架有Hibernate、mybatis等。
什么是 Java 的反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
什么是 ACID
ACID是数据库事务正确执行的四个基本要素的缩写:
原子性(Atomicity),多个操作要么全部成功,要么全部失败
一致性(Consistency),如转账,转出的和收到的金额一致
隔离性(Isolation),多个事务之间互不影响
持久性(Durability),事务执行OK后存入数据库实现持久化
BS与CS的联系与区别
C/S(Client/Server):是指需要安装的客户端应用程序。
B/S(Brower/Server):是指可以用浏览器直接访问的应用程序。
Cookie 和 Session的区别
cookie是服务器存储在本地计算机上的小块文本,浏览器解析cookie并保存为本地文本。
cookie的内容主要包括,名称值、到期时间、路径和域。
生命周期为浏览器会话的cookie为会话cookie,会话cookie通常保存在内存中,浏览器关闭则会话cookie消失,用户其他cookie会保存到硬盘中,下次打开浏览器时读取(但受到到期时间限制),js可以写入和读取cookie。
当程序为客户请求创建会话时,服务器端会先检查客户端是否包含会话ID,如已包含说明客户端之前已经创建了一个会话,如无,则为客户端创建会话并生成会话ID(sessionID),会话ID会返回客户端并保存。
cookie存在客户端,能被客户端程序所窥探和修改;
session(会话)存储在服务器端,不存在信息泄露风险。
关闭浏览器后,session失效(因为session依赖于cookie中的JSESSIONID,到期时间为-1,关闭浏览器则失效),另外服务器也会设置session超时时长,否则太多的session会导致内存溢出。
如何让前端 js 无法使用 cookie,保证网址安全?
有以下两种办法: 1、修改nginx,在 server模块 下面加入以下内容 : add_header Set-Cookie "HttpOnly"; # 在Cookie中设置了"HttpOnly"属性,通过程序(JS、Applet等)将无法读取到Cookie add_header Set-Cookie "Secure"; # 指示浏览器仅通过 HTTPS 连接传回 cookie add_header X-Frame-Options "SAMEORIGIN"; # 不允许一个页面在 <frame>, </iframe> 或者 <object> 中展现的标记 2、java后台 : response.addHeader("Set-Cookie", "mycookie=112; Path=/; HttpOnly"); //设置cookie response.addHeader("Set-Cookie", "mycookie=112; Path=/; Secure; HttpOnly"); //设置https的cookie response.setHeader("x-frame-options", "SAMEORIGIN"); // 设置x-frame-options
fail-fast 与 fail-safe 机制有什么区别
fail-fast(快速失败)和fail-safe(安全失败):之所以会有这两个概念是因为同步修改(当一个或多个线程正在遍历一个集合Collection时,另一个线程修改了这个集合内容<增、删、改>)的存在。 快速失败是在并发修改时抛出ConcurrentModificationException(如多个线程同时操作ArrayList时会有),解决思路是使用java.util.concurrent 包下的集合类(如使用CopyOnWriteArrayList代替ArrayList)。 安全失败是结合在遍历时不直接在集合内容上访问,而是先复制原有集合内容,在拷贝的集合上进行遍历,故不会抛出ConcurrentModificationException。 二者区别:fail-fast(快速失败)是在集合内容上直接遍历,多线程时会报异常;fail-safe(安全失败)是在复本上遍历,浪费内存,同时会造成某个线程读取数据不准确的情况,但不会报异常。
get 和 post请求的区别
w3c“标准答案”: get在浏览器回退时是无害的,而post会再次提交请求; get产生的url可以被浏览器收藏为书签,而post不行; get请求仅支持url编码(application/x-www-form-urlencoded),而post支持多种编码类型(application/x-www-form-urlencoded 或 ultipart/form-data); get请求的参数会被完整地保存在浏览器历史记录中,post参数不会保存在浏览器历史中; get的数据长度的受限(因为URL 的长度是受限制的<URL 的最大长度是 2048 个字符>),post数据长度不受限制; get对数据类型的限制只允许 ASCII 字符,post没有限制,也允许二进制数据; post 比 get 更安全,因为参数不会被保存在浏览器历史或 web 服务器日志中; get 数据在 URL 中对所有人都是可见的,post 数据不会显示在 URL 中。 高手答案: get和post是HTTP协议中的两种发送请求的方法,HTTP底层都是TCP/IP,故get和post都是TCP链接,二者能做的事情其实都是一样的,给get加上request body,给post加上url参数,技术上都是可以实现的。 而导致get和post在“标准答案”中的区别,是由于HTTP的规定和浏览器/服务器的限制,导致他们在应用过程中的不同。 另外,get产生一个TCP数据包,post产生2个TCP数据包: get请求浏览器会把http header 和 data 一并发送出去,服务器返回200;对于post,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok。
因为post发送请求需要两步,故用时会比get更久一点,但并不建议用get替换post。并非所有浏览器都会在post中发送2次包,firefox仅发送一次。
Interface 与 abstract 类的区别
1、接口(Interface)需要被实现,抽象类(abstract类)需要被继承。
2、一个类可以实现多个接口,但一个类只能继承一个抽象类。
3、接口里面的方法全部是抽象的,抽象类里面可以有非抽象的方法。
补充:
接口中的方法和变量必须是public的(其中变量必须是public static final的),抽象类中则可以使private的;
接口中不能包含初始化块(static 块和非静态块),抽象类中可以。
IOC的优点是什么
灵活地提供不同子类的实现(解耦)、提高程序灵活性、可扩展性和可维护性。
IO 和 NIO的区别,NIO优点
IO面向流,阻塞式,NIO面向缓存(通道、缓存),非阻塞式;
在连接不多时,IO编写容易,方便使用,但随着连接数增加,IO会出现瓶颈,因为每个IO连接都需要消耗一个线程,NIO解决了IO的瓶颈问题,可以1个线程处理多个连接,因为非阻塞IO处理连接是异步的,当某个连接发送请求到服务器时,服务器把它当作一个事件,分配给相应的函数处理。
Java 8 / Java 7 为我们提供了什么新功能
Java7 新特性: 1、switch里面的case条件可以使用字符串了 2、运用 List<String> tempList = new ArrayList<>(); 即泛型实例化类型自动推断 Java8 新特性: 1、Java8 允许我们给接口添加一个非抽象的方法实现,只需要使用 default 关键字即可 2、lambda 表达式
什么是竞态条件? 举个例子说明。
程序运行顺序会影响最终结果,并发编程中,由于不恰当的执行时序出现不正确的结果,就是竞态条件。
例如:2个线程同时给一个int值加1,因为同时读取的值,故最终结果可能仅加了1而不是2,程序读取了最后返回的那个线程的值。
解决:在易出现竞态条件的地方使用 synchronized 同步
JRE、JDK、JVM 及 JIT 之间有什么不同
JVM(java 虚拟机):JVM 处理字节码文件,让 java 语言实现跨平台。
JRE(java运行时环境):JRE 是 JVM 的一个超集(JVM对于一个平台或者操作系统是明确的,而JRE却是一个一般的概念,他代表了完整的运行时环境)。
JDK(java开发工具箱):JDK 包含了 JRE 和 Java的开发环境。
JIT(即时编译器):即时编译器是种特殊的编译器,它通过把字节码变成机器码来提高JVM的效率。
MVC的各个部分都有那些技术来实现?如何实现?
Model层:可以用普通的 JavaBean 来实现。
View层:可以用 JSP 或者 JS 来实现。
Controller层:可以用 Struts2 或者 Spring MVC 来实现。
RPC 通信和 RMI 区别
RPC(Remote Procedure Call Protocol)远程过程调用协议,通过网络从远程计算机上请求调用某种服务;
RMI(Remote Method Invocation)远程方法调用,能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端Java 虚拟机中的对象上的方法。
区别:
RMI: 直接获取远端方法的签名,进行调用。优点是强类型、编译期可检查错误;缺点是只限于java语言
RPC: 采用客户端/服务器方式(请求/响应),发送请求到服务器端,服务端执行方法后返回结果。优点是跨语言跨平台,缺点是编译期无法排错,只能在运行时检查。
什么是 Web Service(Web服务)
Web Service 就是通过网络调用其他网站的资源
JSWDL开发包的介绍。JAXP、JAXM的解释。SOAP、UDDI,WSDL解释。
JAXP:(Java API for XML Parsing) 定义了在Java中使用DOM, SAX, XSLT的通用的接口。这样在你的程序中你只要使用这些通用的接口,当你需要改变具体的实现时候也不需要修改代码。
JAXM:(Java API for XML Messaging) 是为SOAP通信提供访问方法和传输机制的API。
SOAP:即简单对象访问协议(Simple Object Access Protocol),它是用于交换XML编码信息的轻量级协议。
UDDI:UDDI的目的是为电子商务建立标准;UDDI是一套基于Web的、分布式的、为Web Service提供的、信息注册中心的实现标准规范,同时也包含一组使企业能将自身提供的Web Service注册,以使别的企业能够发现的访问协议的实现标准。
WSDL:是一种 XML 格式,用于将网络服务描述为一组端点,这些端点对包含面向文档信息或面向过程信息的消息进行操作。这种格式首先对操作和消息进行抽象描述,然后将其绑定到具体的网络协议和消息格式上以定义端点。相关的具体端点即组合成为抽象端点(服务)。
WEB容器主要有哪些功能? 并请列出一些常见的WEB容器名字。
WEB容器的功能:通信支持、管理servlet的生命周期、多线程支持、jsp支持(将jsp翻译成java)
常见的WEB容器:Tomcat、WebLogic、WebSphere
一个”.java”源文件中是否可以包含多个类(不是内部类)?有什么限制
可以,一个“.java”源文件里面可以包含多个类,但是只允许有一个public类,并且类名必须和文件名一致。
简单说说你了解的类加载器。是否实现过类加载器
类加载器负责加载Java类的字节码到Java虚拟机中。
自己实现类加载器一般需要继承 java.lang.ClassLoader ,覆写 findClass(String name)方法。
解释一下什么叫AOP(面向切面编程)
AOP(Aspect Oriented Programming),即面向切面编程,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
所谓"切面",简单说就是将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
请简述 Servlet 的生命周期及其相关的方法
1、实例化阶段:Servlet容器负责加载和实例化Servlet,调用Servlet的构造方法
2、初始化阶段:服务器调用Servlet的init方法进行初始化(只在第一次请求时调用)
3、请求处理阶段:服务器调用Servlet的service方法,然后根据请求方式调用相应的doXXX方法
4、服务终止阶段:服务器调用Servlet的destroy方法销毁Servlet实例
请简述一下 Ajax 的原理及实现步骤
Ajax 即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),通过在后台与服务器进行少量数据交换,可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。
原理:HTTP协议的异步通信
实现步骤:
1、创建一个XMLHttpRequest对象
2、调用该对象的open方法
3、设置回调函数
简单描述Struts的主要功能
1、获取表单内容,并组织生成参数对象
2、根据请求的参数转发请求给适当的控制器
3、在控制器中调用业务接口
4、将业务接口返回的结果包装起来发送给指定的视图,并由视图完成处理结果的展现
5、做一些简单的校验或是国际化工作
什么是 N 层架构
N层架构是一种软件抽象的层次结构,是对复杂软件的一种纵向切分,每一层次中完成同一类型的操作,以便将各种代码根据其完成的使命来进行分割,以降低软件的复杂度,提高其可维护性。
一般来说,层次之间是向下依赖的,下层代码未确定其接口前,上层代码是无法开发的,下层代码接口的变化将使上层的代码一起变化。
什么是CORBA?用途是什么
CORBA(Common Object Request Broker Architecture 公共对象请求代理体系结构)是由OMG组织制订的一种标准的面向对象应用程序体系规范。
用途:
1、存取来自现行桌面应用程序的分布信息和资源
2、使现有业务数据和系统成为可供利用的网络资源
3、为某一特定业务用的定制的功能和能力来增强现行桌面工具和应用程序
4、改变和发展基于网络的系统以反映新的拓扑结构或新资源
什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”
Java虚拟机是执行字节码文件(.class)的虚拟机进程。
因为不同的平台装有不同的Java虚拟机,它们能够将相同的.class文件,解释成不同平台所需要的机器码。所以Java被称为平台无关的编程语言。
什么是正则表达式?用途是什么?哪个包使用正则表达式来实现模式匹配
正则表达式:是对字符串操作的一种逻辑公式,就是用事先定义好的一些特定字符、及这些特定字符的组合,组成一个“规则字符串”,用这个“规则字符串”来表达对字符串的过滤逻辑。
用途包括:
1、字符串匹配
2、指定字符串替换
3、指定字符串查找
4、字符串分割
正则表达式的包:java.util.regex包
什么是懒加载(Lazy Loading)
懒加载:即为延迟加载,顾名思义就是在需要的时候才加载,这样做效率会比较低,但是占用内存低。
什么是尾递归,为什么需要尾递归
如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。
为什么需要尾递归:尾递归和普通递归的不同点在对内存的占用,普通递归创建stack后内存减少,而尾递归只会占用恒量的内存。
(每一个函数在调用下一个函数之前,都能做到先把当前自己占用的栈给先释放了,尾递归的调用链上可以做到只有一个函数在使用栈,因此可以无限地调用!尾递归的实现依赖于编译器的帮助<或者说语言的规定>)
什么是控制反转(Inversion of Control)与依赖注入(Dependency Injection)
控制反转:是指将创建对象的功能交给Spring容器,在我们需要使用对象的时候不需要自己创建,可以直接从容器中获取。
依赖注入:动态的向某个对象提供它所依赖的其他对象。
关键字
finalize
什么是finalize()方法
Java 可以使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做一些必要的清理工作。
finalize()方法什么时候被调用
这个方法是由垃圾收集器在确定这个对象没有被引用时 对这个对象进行调用的。
析构函数(finalization)的目的是什么
析构函数的目的是:在清除对象前,完成一些清理工作,比如:释放内存等。
final 和 finalize 的区别
final关键字可以用于类、方法、变量前,用来表示该类、方法、变量具有不可变的特性。
finalize方法用于回收资源,可以为任何一个类添加finalize方法。该方法将在垃圾回收器清除对象之前调用。
final
final关键字有哪些用法
用来修饰变量,表示该变量的值无法改变;
用来修饰方法,表示该方法无法被重写;
用来修饰类,表示该类无法被继承。
final 与 static 关键字可以用于哪里?它们的作用是什么
final:
用来修饰变量,表示该变量的值无法改变;
用来修饰方法,表示该方法无法被重写;
用来修饰类,表示该类无法被继承。
static:
静态变量,可以用类名直接访问
静态方法,可以通过类名直接调用
volatile 修饰符的有过什么实践
1,当写一个volatile变量时,JMM(java内存模型)会把该线程本地内存中的所有共享变量刷新到主内存中去 2,当读取一个volatile变量时,该线程会将本地内存置为无效,线程将从主内存中读取共享变量。 总结,volatile变量可以实现线程之间的通信。 当对一个volatile变量写操作时,实际上就是指明我对主内存中的共享变量产生更改,接下来读取这个volatile变量的线程就会从主线程中取回最新的共享变量值。 一种实践是用 volatile 修饰 long 和 double 变量,使其能按原子类型来读写。double 和 long 都是64位宽,因此对这两种类型的读是分为两部分的,第一次读取第一个 32 位,然后再读剩下的 32 位,这个过程不是原子的,但 Java 中 volatile 型的 long 或 double 变量的读写是原子的。volatile 修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个 volatile 变量之前,Java 内存模型会插入一个写屏障(write barrier),读一个 volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个 volatile 域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。
volatile 类型变量提供什么保证?能使得一个非原子操作变成原子操作吗
volatile 可以保证 对象的可见性 和 程序的顺序性,无法保证操作的原子性。
能创建 volatile 数组吗?
volatile修饰的变量如果是对象或数组之类的,其含义是对象获数组的地址具有可见性,但是数组或对象内部的成员改变不具备可见性
transient变量有什么特点
java 的transient关键字的作用是需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。
public static void 写成 static public void会怎样
public static void 可以写成 static public void
但不能写成 public void static
a = a + b 与 a += b 的区别?
+= 隐式的将加操作的结果类型强制转换为持有结果的类型。如果两这个整型相加,如 byte、short 或者 int,首先会将它们提升到 int 类型,然后在执行加法操作。 byte a = 127;byte b = 127;b = a + b; // error : cannot convert from int to byteb += a; // ok (因为 a+b 操作会将 a、b 提升为 int 类型,所以将 int 类型赋值给 byte 就会编译出错)
逻辑操作符 (&,|,^)与条件操作符(&&,||)的区别
a.条件操作只能操作布尔型的,而逻辑操作不仅可以操作布尔型,而且可以操作数值型 b.逻辑操作不会产生短路.如: int a = 0; int b = 0; if( (a = 3) > 0 || (b = 3) > 0 ) //操后a =3,b=0. if( (a = 3) > 0 | (b = 3) > 0 ) //操后a =3,b=3。
3*0.1 == 0.3 将会返回什么?true 还是 false?
false,因为有些浮点数不能完全精确的表示出来 (float)3*(float)0.1 == (float)0.3 ----> true 4*0.1 == 0.4 ----> true 所以对于小数,需要比较时,最稳的做法是使用BigDecimal,但是BigDecimal也存在问题 new BigDecimal(3).multiply(new BigDecimal(0.1)).compareTo(new BigDecimal(0.3)) == 0 ----> false // 使用String入参 new BigDecimal("3").multiply(new BigDecimal("0.1")).compareTo(new BigDecimal("0.3")) == 0 ----> true
float f=3.4; 是否正确?
不正确。 3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型,会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4f;
short s1 = 1; s1 = s1 + 1;有什么错?
前者有错,后者正确。 s1 short型 通过 + 运算后s1 自动转为int 型 所以错; += 是复合赋值操作符,复合赋值等价于简单赋值,所以s1+=1等效于s1=(short)(s1+1)。
如何去小数四舍五入保留小数点后两位
(1)BigDecimal b = new BigDecimal(f); double f1 = b.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); (2)new java.text.DecimalFormat("#.00").format(3.1415926); (3)double d = 3.1415926; String result = String.format("%.2f", d); (4)Math.round(5.2644555 * 100) * 0.01d;
char 型变量中能不能存贮一个中文汉字,为什么
在Java中,char类型占2个字节,而且Java默认采用Unicode编码,一个Unicode码是16位,所以一个Unicode码占两个字节,Java中无论汉子还是英文字母都是用Unicode编码来表示的。
所以,在Java中,char类型变量可以存储一个中文汉字。
我们能将 int 强制转换为 byte 类型的变量吗?如果该值大于 byte 类型的范围,将会出现什么现象
是的,我们可以做强制转换,但是 Java 中 int 是 32 位的,而 byte 是 8 位的,所以,如果强制转化,int 类型的高 24 位将会被丢弃,byte 类型的范围是从 -128 到 127。
如何权衡是使用无序的数组还是有序的数组
有序数组查询容易,插入难。无序数组插入容易,查询难。
简述 ConcurrentLinkedQueue LinkedBlockingQueue 的用处和不同之处。
Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。
ConcurrentLinkedQueue 是非阻塞队列的代表,没有锁,使用CAS实现,有 add(e)、offer(e)、remove()、poll()和peek(),size() 是要遍历一遍集合的,速很慢,所以判空时,尽量要避免用size(),而改用isEmpty()。
LinkedBlockingQueue 是阻塞队列,内部则是基于锁,增加了 put(e)、take() 两个阻塞方法。
LinkedBlockingQueue 多用于任务队列,ConcurrentLinkedQueue 多用于消息队列
单生产者,单消费者 用 LinkedBlockingqueue
多生产者,单消费者 用 LinkedBlockingqueue
单生产者 ,多消费者 用 ConcurrentLinkedQueue
多生产者 ,多消费者 用 ConcurrentLinkedQueue
HashMap的工作原理是什么
HashMap使用的是哈希表(也称散列表,是根据关键码值(Key value)而直接进行访问的数据结构),哈希表有多种不同的实现方法,常用的实现方式是“数组+链表”实现,即“链表的数组”。 首先,HashMap中存储的对象为数组Entry[](每个Entry就是一个<key,value>),存和取时根据 key 的 hashCode 相关的算法得到数组的下标,put 和 get 都根据算法的下标取得元素在数组中的位置。 万一两个key分别有相同的下标,那么怎么处理呢? 使用链表,把下标相同的 Entry 放到一个链表中,存储时,存到第一个位置,并把next指向之前的第一个元素(如果是已存在的key,则需要遍历链表),取值时,遍历链表,通过equals方法获取Entry。 // 存储时: int hash = key.hashCode(); // 这个hashCode方法这里不详述,只要理解每个key的hash是一个固定的int值 int index = hash & (Entry[].length-1); // 二进制的按位与运算 Entry[index] = value; // 取值时: int hash = key.hashCode(); int index = hash & (Entry[].length-1); // 二进制的按位与运算 return Entry[index]; // 此处需要遍历 Entry[index] 的链表取值,此处为简写 关于原理细节,可参考文章 https://www.cnblogs.com/holyshengjie/p/6500463.html 前半部分 关于源码实现,可参考文章 https://www.cnblogs.com/chengxiao/p/6059914.html 【关于为什么覆盖 equals方法 后需要同时覆盖 hashCode方法】 在此文末尾 通过以上理解,如果 hashCode方法 写得不好,比如所有 key 对象 都返回 同一个 int 值,那么效率也不会高,因为就相当于一个链表了。
HashMap 的 table的容量如何确定?loadFactor 是什么? 该容量如何变化?这种变化会带来什么问题?
①、table 数组大小是由 capacity 这个参数确定的,默认是16,也可以构造时传入,最大限制是1<<30; ②、loadFactor 是装载因子,主要目的是用来确认table 数组是否需要动态扩展,默认值是0.75,比如table 数组大小为 16,装载因子为 0.75 时,threshold 就是12,当 table 的实际大小超过 12 时,table就需要动态扩容; ③、扩容时,调用 resize() 方法,将 table 长度变为原来的两倍(注意是 table 长度,而不是 threshold) ④、如果数据很大的情况下,扩展时将会带来性能的损失,在性能要求很高的地方,这种损失很可能很致命。 参考自:https://www.jianshu.com/p/75adf47958a7
HashMap 和 HashTable、ConcurrentHashMap 的区别
这个问题面试的时候很常见。 参考: https://blog.csdn.net/ZytheMoon/article/details/88376749
参考: https://www.jianshu.com/p/c00308c32de4
HashSet和TreeSet有什么区别
相同点: 单列集合,元素不可重复 不同点 1. 底层存储的数据结构不同 HashSet底层用的是HashMap哈希表结构存储,而TreeSet底层用的是TreeMap树结构存储 2.存储时保证数据唯一性依据不同 HashSet是通过复写hashCode()方法和equals()方法来保证的,而TreeSet通过Compareable接口的compareTo()方法来保证的 3.有序性不一样 HashSet无序,TreeSet有序 存储原理: HashSet:底层数据结构是哈希表,本质就是对哈希值的存储,通过判断元素的hashCode方法和equals方法来保证元素的唯一性,当hashCode值不相同,就直接存储了,不用在判断equals了,当hashCode值相同时,会在判断一次euqals方法的返回值是否为true,如果为true则视为用一个元素,不用存储,如果为false,这些相同哈希值不同内容的元素都存放一个桶里(当哈希表中有一个桶结构,每一个桶都有一个哈希值) TreeSet:底层的数据结构是二叉树,可以对Set集合中的元素进行排序,这种结构,可以提高排序性能, 根据比较方法的返回值确定的,只要返回的是0.就代表元素重复
HashSet 内部是如何工作的
HashSet 的内部采用 HashMap来实现。由于 Map 需要 key 和 value,所以所有 key 的都有一个默认 value。类似于 HashMap,HashSet 不允许重复的 key,只允许有一个null key,意思就是 HashSet 中只允许存储一个 null 对象。
WeakHashMap 是怎么工作的?
WeakHashMap实现了Map接口,是HashMap的一种实现,他使用弱引用作为内部数据的存储方案,WeakHashMap可以作为简单缓存表的解决方案,当系统内存不够的时候,垃圾收集器会自动的清除没有在其他任何地方被引用的键值对。
Set
Set 里的元素是不能重复的,那么用什么方法来区分重复与否呢?是用 == 还是 equals()? 它们有何区别?
HashSet 根据 hashCode()方法和equals() 方法确定元素是否重复;TreeSet 根据 compareTo()方法
TreeMap和TreeSet在排序时如何比较元素?Collections工具类中的sort()方法如何比较元素?
TreeSet要求存放的对象所属的类必须实现Comparable接口,该接口提供了比较元素的compareTo()方法,当插入元素时会回调该方法比较元素的大小。
TreeMap要求存放的键值对映射的键必须实现Comparable接口从而根据键对元素进行排序。
Collections工具类的sort方法有两种重载的形式,第一种要求传入的待排序容器中存放的对象必须实现Comparable接口以实现元素的比较;第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator接口的子类型(需要重写compare方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java中对函数式编程的支持)。
一个已经构建好的 TreeSet,怎么完成倒排序。
使用Collections工具类中的sort()方法,实现Comparator接口,内部排序和treeSet的相反即可。
简述一致性 Hash 算法
通过环形Hash空间把数据、机器等通过一定的hash算法处理后映射到环上,通过虚拟节点保证平衡性 一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义: 1、平衡性(Balance) 2、单调性(Monotonicity) 3、分散性(Spread) 4、负载(Load) 普通的哈希算法(也称硬哈希)采用简单取模的方式,将机器进行散列,这在cache环境不变的情况下能取得让人满意的结果,但是当cache环境动态变化时,这种静态取模的方式显然就不满足单调性的要求(当增加或减少一台机子时,几乎所有的存储内容都要被重新散列到别的缓冲区中)。 一致性哈希算法的基本实现原理是将机器节点和key值都按照一样的hash算法映射到一个0~2^32的圆环上。当有一个写入缓存的请求到来时,计算Key值k对应的哈希值Hash(k),如果该值正好对应之前某个机器节点的Hash值,则直接写入该机器节点,如果没有对应的机器节点,则顺时针查找下一个节点,进行写入,如果超过2^32还没找到对应节点,则从0开始查找(因为是环状结构)。
Object有哪些公用方法
clone、equals、hashCode、getClass、wait、notify、notifyAll、toString
LinkedHashMap 和 PriorityQueue 的区别是什么
PriorityQueue保证最高或者最低优先级的的元素总是在队列头部,但是LinkedHashMap维持的顺序是元素插入的顺序。当遍历一个PriorityQueue 时,没有任何顺序保证,但是LinkedHashMap 课保证遍历顺序是元素插入的顺序。
LinkedList 是单向链表还是双向链表
双向
ArrayList 和 HashMap 的默认大小是多数
在Java7中,ArrayList的默认大小是10个元素,HashMap的默认大小是16个元素(必须是2的幂) ArrayList:内部实现是一个Object的数组。初始默认大小为0,当然也可以在其构造方法中设置。当添加一个Object时,默认扩充数组容量为10。然后每次扩充的新的数组大小等于,(原始容量*3/2)和(数组的长度+1)之间的较大值。根据每次增加一个Object,可得该情况每次扩充的固定大小为3/2。当初始大小为手动设置的时候,每次扩充的新的数组大小等于,(原始容量*3/2)和(数组的长度+1)之间的较大值。 HashMap:内部实现是一个Entry的数组,默认大小是空的数组。初始化的容量是16,负载因子是0.75(当数组元素数量大于总容量的负载因子的时候,扩充数组)。当默认不是空的数组时,当达到负载因子的比例的时候,每次扩充初始容量的2倍。
Comparator 与 Comparable 接口是干什么的?列出它们的区别
Comparable & Comparator 都是用来实现集合中元素的比较、排序的,只是 Comparable 是在集合内部定义的方法实现的排序,Comparator 是在集合外部实现的排序。
comparable接口:
优点:对于单元素集合可以实现直接排序
缺点:对于多元素排序,排序依据是固定不可更改的。(元素内部只能实现一次compareTo方法)
comparator接口:
元素的排序依据时刻变的,所以可以通过定义多个外部类的方式来实现不同的排序。使用时按照需求选取。
创建外部类的代价太大。
如何实现对象克隆
1). 实现Cloneable接口并重写Object类中的clone()方法; 2). 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆
深拷贝和浅拷贝区别
简单的来说就是,在有指针的情况下,浅拷贝只是增加了一个指针指向已经存在的内存,而深拷贝就是增加一个指针并且申请一个新的内存,使这个增加的指针指向这个新的内存,采用深拷贝的情况下,释放内存的时候就不会出现在浅拷贝时重复释放同一内存的错误,修改对象也是
写clone()方法时,通常都有一行代码,是什么
super.clone();
如何构建不可变的类结构?关键点在哪里
(1)将类声明为final,所以它不能被继承 (2)将所有的成员声明为私有的,这样就不允许直接访问这些成员 (3)对变量不要提供setter方法 (4)将所有可变的成员声明为final,这样只能对它们赋值一次 (5)通过构造器初始化所有成员,进行深拷贝(deep copy) (6)在getter方法中,不要直接返回对象本身,而是克隆对象,并返回对象的拷贝
能创建一个包含可变对象的不可变对象吗
可以的,我们是可以创建一个包含可变对象的不可变对象的,你只需要谨慎一点,不要共享可变对象的引用就可以了,如果需要变化时,就返回原对象的一个拷贝。
set属性时,不要这样:this.person = person; 而要这样:this.person = new Person(person.getName(),person.getAge()); ---> 因为传入的 person 对象随时会被外界修改
方法可以同时即是 static 又是 synchronized 的吗
可以。如果这样做的话,JVM会获取和这个对象关联的java.lang.Class实例上的锁。这样做等于:
synchronized(XYZ.class) {
}
如果main方法被声明为private会怎样
可以编译,运行时报错
垃圾回收的最佳做法是什么
1、标记-清除算法: 首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。标记过程中 实际上即时上面说的finaLize()的过程。主要缺点一个是效率问题。另外一个是空间问题,标记清除后会产生大量不连续的内存碎片。 2、复制算法: 这种算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了。就将还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉。 3、标记-整理算法: 复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会遍低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以对应被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。 标记过程仍然与标记-清除算法一样,但是后续步骤不是直接将对可回收对象进行清理,而是让所有存活的对象都向领一端移动,然后直接清理掉端边界以外的内存。 4、分代收集算法: 当代商业虚拟机的垃圾收集都采用的是“分代收集算法” ,根据对象的存活周期的不同,将内存化为几块,一般是把java堆分为新生代和老年代。这样就可以根据各个年代的特点采用最合适的收集算法。 新生代选用复制算法,老年代使用标记-清理算法 或者 标记-整理算法。
GC收集器有哪些
1、serial收集器 serial是一个单线程的垃圾收集器,它只会使用一个cpu、一个收集线程工作;它在进行gc的时候,必须暂停其他所有线程到它工作结束(这种暂停往往让人难以接受)。 对于单个cpu的情况下,serial是没有线程交互的开销,效率要好于其他。例如在一个简单的桌面应用,只有几十M的内存,他的停顿时间可能只有几十毫秒,所以一般默认的client模式下都是用的serial做默认垃圾收集器。 2、parnew收集器 parnew其实就是serial的多线程版本,parnew在单线程的情况下甚至不如serial,parnew是除了serial之外唯一能和CMS配合的。 parnew默认开启收集线程数和cpu的数量相同,我们可以利用-XX:ParallelGCThreads参数来控制它开启的收集线程数。 3、parallel scavenge收集器 parallel scavenge主要就是关注吞吐量。 所谓吞吐量:运行用户代码的世界/(运行用户代码时间+GC花费的时间)。 parallel scavenge收集器中,提供了2个参数来控制吞吐量: -XX:GCTimeRatio:gc时间占用的总比例,也就是吞吐量的倒数。 -XX:MaxGCPauseMillis:最大的暂停毫秒数(这个数值并非越小越好,如果把他设置小了,系统会根据这个值调整空间的大小,也就会加快GC的频率) parallel scavenge可以设置开启-XX:UseAdaptiveSizePolicy,开启这个参数之后就无需关注新生代大小eden和survivor等比例,晋升老年代对象年龄的这些细节了。 4、serial old收集器 serial收集器的老年代版本,使用标记整理算法,主要有两个作用: jdk5之前和parallel scavenge配合使用 作为cms失败的后备收集方案 5、parallel old收集器 是parallel收集器的老年代版本,用于和parallel收集器搭配组合的,因为parallel收集器不能和cms组合,但是和serial old收集器效率又太低。 对吞吐量和CPU敏感度特别关注的应用可以使用parallel+parallel old的组合。 6、CMS收集器、特点、过程、缺点 CMS的适用特点: 希望JAVA垃圾回收器回收垃圾的时间尽可能短; 应用运行在多CPU的机器上,有足够的CPU资源; 有比较多生命周期长的对象; 希望应用的响应时间短。 CMS的过程: 初始标记:标记一下GC ROOT能直接关联的对象,速度很快,这个阶段是会STW。 并发标记:GC ROOT的引用链的查找过程,标记能关联到GC ROOT的对象,这一个阶段是不需要STW的。 重新标记:在并发标记阶段,应用的线程可能产生新的垃圾,所以需要重新标记,这个阶段也是会STW。 并发清除:这个阶段就是真正的回收垃圾的对象的阶段,无需STW。 CMS的缺点: 对cpu比较敏感。 可能出现浮动垃圾:在并发清除阶段,用户还是继续使用的,这时候就会有新的垃圾出现,CMS只能等下一次GC才能清除掉他们。 CMS运行期间预留内存不够的话,就会出现concurrent Mode Failure,这时候就会启动serial收集器进行收集。 CMS基于标记清除算法实现,会产生内存碎片空间。碎片空间过多就会对大对象的分配空间造成麻烦。为了解决碎片问题,CMS提供一个参数来控制是否在GC的时候整合内存中的碎片,这个碎片整合的操作是无法并发的,会延长STW的时间。 7、G1收集器 G1的特点: 利用多CPU来缩短STW的时间 可以独立管理整个堆(使用分代算法) 整体是基于标记-整理,局部使用复制算法,不会产生碎片空间 可以预测停顿:G1吧整个堆分成多个Region,然后计算每个Region里面的垃圾大小(根据回收所获得的空间大小和回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。 G1的运行过程: 初始标记:标记一下GC ROOT能直接关联的对象,速度很快,这个阶段是会STW。 并发标记:在GC ROOT中运用可达性分析算法,找出存活的对象,耗时较长,但是无需STW。 最终标记:修正并发标记期间用户线程对垃圾对象的修改,需要停顿线程,但是可以并行执行。 筛选回收:先计算回收各个Region的价值,然后根据用户需求来进行回收。
串行(serial)收集器和吞吐量(throughput)收集器的区别是什么
串行GC:整个扫描和复制过程均采用单线程的方式,相对于吞吐量GC来说简单;适合于单CPU、客户端级别。
吞吐量GC:采用多线程的方式来完成垃圾收集;适合于吞吐量要求较高的场合,比较适合中等和大规模的应用程序。
吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。而串行收集器对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。
JVM的永久代中会发生垃圾回收吗
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发完全垃圾回收(Full GC)。如果你仔细查看垃圾收集器的输出信息,就会发现永久代也是被回收的。这就是为什么正确的永久代大小对避免Full GC是非常重要的原因。
标记清除、标记整理、复制算法的原理与特点?分别用在什么地方
1:标记清除:直接将要回收的对象标记,发送gc的时候直接回收:特点回收特别快,但是回收以后会造成很多不连续的内存空间,因此适合在老年代进行回收,CMS(current mark-sweep),就是采用这种方法来会后老年代的。 2:标记整理:就是将要回收的对象移动到一端,然后再进行回收,特点:回收以后的空间连续,缺点:整理要花一定的时间,适合老年代进行会后,parallel Old(针对parallel scanvange gc的) gc和Serial old就是采用该算法进行回收的。 3:复制算法:将内存划分成原始的是相等的两部分,每次只使用一部分,这部分用完了,就将还存活的对象复制到另一块内存,将要回收的内存全部清除。这样只要进行少量的赋值就能够完成收集。比较适合很多对象的回收,同时还有老年代对其进行担保。(serial new和parallel new和parallel scanvage)
说说你知道的几种主要的jvm 参数
-Xms3550m:设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -Xmx3550m:设置JVM最大可用内存为3550M -Xss128k: 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。 -XX:MaxPermSize=16m :设置持久代大小为16m -XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5 -XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6 -XX:+UseParallelOldGC :配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
Java 类加载器都有哪些
1)Bootstrap ClassLoader 负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现,不是ClassLoader子类 2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目录下的jar包 3)App ClassLoader 负责记载classpath中指定的jar包及目录中class 4)Custom ClassLoader 属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现ClassLoader 加载过程中会先检查类是否被已加载,检查顺序是自底向上,从Custom ClassLoader到BootStrap ClassLoader逐层检查,只要某个classloader已加载就视为已加载此类,保证此类只所有ClassLoader加载一次。而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
JVM如何加载字节码文件
JVM主要完成三件事: 1、通过一个类的全限定名(包名与类名)来获取定义此类的二进制字节流(Class文件)。而获取的方式,可以通过jar包、war包、网络中获取、JSP文件生成等方式。 2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。这里只是转化了数据结构,并未合并数据。(方法区就是用来存放已被加载的类信息,常量,静态变量,编译后的代码的运行时内存区域) 3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。这个Class对象并没有规定是在Java堆内存中,它比较特殊,虽为对象,但存放在方法区中。
JVM内存分哪几个区,每个区的作用是什么
1、程序计数器(Program Counter Register) 程序计数器是一个比较小的内存区域,用于指示当前线程所执行的字节码执行到了第几行,可以理解为是当前线程的行号指示器。字节码解释器在工作时,会通过改变这个计数器的值来取下一条语句指令。 每个程序计数器只用来记录一个线程的行号,所以它是线程私有(一个线程就有一个程序计数器)的。 如果程序执行的是一个Java方法,则计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是一个本地(native,由C语言编写完成)方法,则计数器的值为Undefined,由于程序计数器只是记录当前指令地址,所以不存在内存溢出的情况,因此,程序计数器也是所有JVM内存区域中唯一一个没有定义OutOfMemoryError的区域。 2、虚拟机栈(JVM Stack) 一个线程的每个方法在执行的同时,都会创建一个栈帧(Statck Frame),栈帧中存储的有局部变量表、操作站、动态链接、方法出口等,当方法被调用时,栈帧在JVM栈中入栈,当方法执行完成时,栈帧出栈。 局部变量表中存储着方法的相关局部变量,包括各种基本数据类型,对象的引用,返回地址等。在局部变量表中,只有long和double类型会占用2个局部变量空间(Slot,对于32位机器,一个Slot就是32个bit),其它都是1个Slot。需要注意的是,局部变量表是在编译时就已经确定好的,方法运行所需要分配的空间在栈帧中是完全确定的,在方法的生命周期内都不会改变。 虚拟机栈中定义了两种异常,如果线程调用的栈深度大于虚拟机允许的最大深度,则抛出StatckOverFlowError(栈溢出);不过多数Java虚拟机都允许动态扩展虚拟机栈的大小(有少部分是固定长度的),所以线程可以一直申请栈,直到内存不足,此时,会抛出OutOfMemoryError(内存溢出)。 每个线程对应着一个虚拟机栈,因此虚拟机栈也是线程私有的。 3、本地方法栈(Native Method Statck) 本地方法栈在作用,运行机制,异常类型等方面都与虚拟机栈相同,唯一的区别是:虚拟机栈是执行Java方法的,而本地方法栈是用来执行native方法的,在很多虚拟机中(如Sun的JDK默认的HotSpot虚拟机),会将本地方法栈与虚拟机栈放在一起使用。 本地方法栈也是线程私有的。 4、堆区(Heap) 堆区是理解Java GC机制最重要的区域,没有之一。在JVM所管理的内存中,堆区是最大的一块,堆区也是Java GC机制所管理的主要内存区域,堆区由所有线程共享,在虚拟机启动时创建。堆区的存在是为了存储对象实例,原则上讲,所有的对象都在堆区上分配内存(不过现代技术里,也不是这么绝对的,也有栈上直接分配的)。 一般的,根据Java虚拟机规范规定,堆内存需要在逻辑上是连续的(在物理上不需要),在实现时,可以是固定大小的,也可以是可扩展的,目前主流的虚拟机都是可扩展的。如果在执行垃圾回收之后,仍没有足够的内存分配,也不能再扩展,将会抛出OutOfMemoryError:Java heap space异常。 5、方法区(Method Area) 在Java虚拟机规范中,将方法区作为堆的一个逻辑部分来对待,但事实上,方法区并不是堆(Non-Heap);另外,不少人的博客中,将Java GC的分代收集机制分为3个代:青年代,老年代,永久代,这些作者将方法区定义为“永久代”,这是因为,对于之前的HotSpot Java虚拟机的实现方式中,将分代收集的思想扩展到了方法区,并将方法区设计成了永久代。不过,除HotSpot之外的多数虚拟机,并不将方法区当做永久代,HotSpot本身,也计划取消永久代。本文中,由于笔者主要使用Oracle JDK6.0,因此仍将使用永久代一词。 方法区是各个线程共享的区域,用于存储已经被虚拟机加载的类信息(即加载类时需要加载的信息,包括版本、field、方法、接口等信息)、final常量、静态变量、编译器即时编译的代码等。 方法区在物理上也不需要是连续的,可以选择固定大小或可扩展大小,并且方法区比堆还多了一个限制:可以选择是否执行垃圾收集。一般的,方法区上执行的垃圾收集是很少的,这也是方法区被称为永久代的原因之一(HotSpot),但这也不代表着在方法区上完全没有垃圾收集,其上的垃圾收集主要是针对常量池的内存回收和对已加载类的卸载。 在方法区上进行垃圾收集,条件苛刻而且相当困难,效果也不令人满意,所以一般不做太多考虑,可以留作以后进一步深入研究时使用。 在方法区上定义了OutOfMemoryError:PermGen space异常,在内存不足时抛出。 运行时常量池(Runtime Constant Pool)是方法区的一部分,用于存储编译期就生成的字面常量、符号引用、翻译出来的直接引用(符号引用就是编码是用字符串表示某个变量、接口的位置,直接引用就是根据符号引用翻译出来的地址,将在类链接阶段完成翻译);运行时常量池除了存储编译期常量外,也可以存储在运行时间产生的常量(比如String类的intern()方法,作用是String维护了一个常量池,如果调用的字符“abc”已经在常量池中,则返回池中的字符串地址,否则,新建一个常量加入池中,并返回地址)。
解释内存中的栈(stack)、堆(heap)和方法区(method area)的用法
堆区: 1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令) 2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身 栈区: 1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中 2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。 3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。 方法区: 1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。 2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
简述内存分配与回收策略
(1)对象优先分配到新生代的Eden区 (2)大对象直接进入老年代【JVM中提供了一个-XX:PretenureSizeThreshold参数(这个参数只对Serial和ParNew这两个新生代垃圾收集器有效),令大于这个参数的对象直接在老年代中分配】 (3)长期存活的对象将进入老年代【每熬过一次GC,年龄+1,当这个值到达一个阀值(默认15,可通过-XX:MaxTenuringThreshold来设置)时,这个对象就会被移到老年代中】 (4)动态对象年龄判断【JVM也不是要去一个对象必须达到MaxTenuringThreshold设置的年龄阀值才能进入老年代,如果Survivor中的对象满足同年龄(比如N)对象所占空间达到了Survivor总空间的一半的时候,那么年龄大于或者等于N的对象都可以进入老年代,无需等待阀值】 (5)空间分配担保【新生代采用复制算法,但是会造成空间的浪费,故而提出了一种“空间担保机制”来提高复制算法的空间利用率,使复制算法的浪费从50%降到了10%。而老年代的内存就充当了这个担保者,并且由于没有其他内存来担保老年代,所以老年代如果不想产生空间内存碎片那么只能使用“标记-整理”算法了】
简述重排序,内存屏障,happen-before,主内存,工作内存
重排序:大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。通过乱序执行的技术,处理器可以大大提高执行效率。
内存屏障:内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
happen—before(是一个原则):
Java内存模型中定义的两项操作之间的偏序关系,如果操作A先行发生于操作B,其意思就是说,在发生操作B之前,操作A产生的影响都能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等,它与时间上的先后发生基本没有太大关系。这个原则特别重要,它是判断数据是否存在竞争、线程是否安全的主要依据。
简述 主内存 和 工作内存
JVM将内存组织为主内存和工作内存两个部分。 主内存主要包括本地方法区和堆。 每个线程都有一个工作内存,工作内存中主要包括两个部分,一个是属于该线程私有的栈和对主存部分变量拷贝的寄存器(包括程序计数器PC和cup工作的高速缓存区)。 1.所有的变量都存储在主内存中(虚拟机内存的一部分),对于所有线程都是共享的。 2.每条线程都有自己的工作内存,工作内存中保存的是主存中某些变量的拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。 3.线程之间无法直接访问对方的工作内存中的变量,线程间变量的传递均需要通过主内存来完成。
Java中存在内存泄漏问题吗?请举例说明
一般来讲,内存泄漏主要有两种情况: 一是在堆中申请了空间没有被释放; 二是对象已不再被使用,但还仍然在内存中保留着。 垃圾回收机制的引入可以有效地解决第一种情况;而对于第二种情况,垃圾回收机制则无法保证不再使用的对象会被释放。因此Java语言中的内存泄漏主要指的第二种情况。 Java语言中,容易引起内存泄漏的原因有很多,主要有以下几个方面的内容: (1)静态集合类,例如HashMap和Vector。如果这些容器为静态的,由于它们的声明周期与程序一致,那么容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。 (2)各种连接,例如数据库的连接、网络连接以及IO连接等。 (3)监听器。在Java语言中,往往会使用到监听器。通常一个应用中会用到多个监听器,但在释放对象的同时往往没有相应的删除监听器,这也可能导致内存泄漏。 (4)变量不合理的作用域。一般而言,如果一个变量定义的作用域大于其使用范围,很有可能会造成内存泄漏,另一方面如果没有及时地把对象设置为Null,很有可能会导致内存泄漏的放生 (5)单例模式可能会造成内存泄漏
简述 Java 中软引用(SoftReferenc)、弱引用(WeakReference)和虚引用
强引用(StrongReference):强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它
软引用(SoftReference):如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
弱引用(WeakReference):只具有弱引用的对象比软引用的拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
虚引用(PhantomReference):形同虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
jstack,jstat,jmap,jconsole怎么用
jstat 用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计【类的加载及卸载情况,新生代、老生代及持久代的容量及使用情况,垃圾收集情况 等】 jstat -gcutil pid 用于查看新生代、老生代及持代垃圾收集的情况 jstat -gc pid 查看指定进程的gc情况 jstat -class pid 显示加载class的数量,及所占空间等信息 jstack 主要用来查看某个Java进程内的线程堆栈信息,在发生死锁时可以用jstack -l pid来观察锁持有情况 jmap 用来查看堆内存使用状况,一般结合jhat使用 使用jmap -heap pid查看进程堆内存使用情况,包括使用的GC算法、堆配置参数和各代中堆内存使用情况 jmap -histo pid 打印每个class的实例数目,内存占用,类全名信息 jmap -dump:format=b,file=myObj.log pid 使用hprof二进制形式,输出jvm的heap(堆)内容到文件 jconsole 一个java GUI监视工具,可以以图表化的形式显示各种数据。
32 位 JVM 和 64 位 JVM 的最大堆内存分别是多数?32 位和 64 位的 JVM,int 类型变量的长度是多数?
理论上说上 32 位的 JVM 堆内存可以到达 2^32,即 4GB,但实际上会比这个小很多,不同操作系统之间不同。 64 位 JVM允许指定最大的堆内存,理论上可以达到 2^64,这是一个非常大的数字,实际上你可以指定堆内存大小到 100GB。 32 位和 64 位的 JVM 中,int 类型变量的长度是相同的,都是 32 位或者 4 个字节。
怎样通过 Java 程序来判断 JVM 是 32 位 还是 64 位
System.out.println("jdk的版本为:" + System.getProperty("sun.arch.data.model") );
什么情况下会发生栈内存溢出
与线程栈相关的内存异常有两个:
a)StackOverflowError(栈溢出,方法调用层次太深,内存不够新建栈帧)
b)OutOfMemoryError(线程太多,内存不够新建线程)
双亲委派模型是什么
双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
其中,类加载器包括:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader)
为什么需要双亲委派模型呢?
试想一个场景:黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。
如何实现双亲委派模型?
每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,我们无需去写。 我们可以自定义我们的类加载器,只需要 extends ClassLoader,覆盖 findClass 方法即可 我们可以在代码里面查看使用的是哪个类加载器:System.out.println("恩,是的,我是由 " + getClass().getClassLoader().getClass() + " 加载进来的"); 其他代码: MyClassLoader classLoader = new MyClassLoader("D:/test"); // 入参是 classPath Class clazz = classLoader.loadClass("com.huachao.cl.Test"); Object obj = clazz.newInstance(); Method helloMethod = clazz.getDeclaredMethod("hello", null);
如何打破双亲委派模型?
如果不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
而如果想打破双亲委派模型则需要重写 ClassLoader 类中的 loadClass() 方法(当然其中的坑也不会少)。
典型打破双亲委派的例子:
tomcat 为了实现隔离性(一个tomcat下面可能部署了多个项目,多个项目之间如果有同包同名的class,需要各个项目单独隔离加载),故 WebappClassLoader.loadClass() 加载自己的目录下的class文件,不会传递给父类加载器
补充:tomcat 的 WebappClassLoader 是 各个Webapp私有的类加载器,加载每个项目下的 WEB-INFO/lib 和 WEB-INFO/class 路径下的 class
数据库事务隔离级别
读未提交:可以读到其他事务未提交的内容,所以会有脏读、不可重复读、幻读 存在。 读已提交:只能读到已经提交了的内容,避免了脏读,但是不可重复读、幻读 依然存在,原理是使用的快照读。 可重复读:专门针对“不可重复读”这种情况而制定的隔离级别(还是有幻读 <由insert、delete产生> 存在),MySql的默认隔离级别,原理是当事务启动时不允许进行update(但可以 delete、insert)。 串行化:事务“串行化顺序执行”,也就是一个一个排队执行。脏读、不可重复读、幻读 都可避免,但是效率奇差。
多线程的几种实现方式
继承Thread类创建线程
实现Runnable接口创建线程
实现Callable接口通过FutureTask包装器来创建Thread线程 【FutureTask是 Future接口和 Runnable 接口 的 实现类,是Future接口的一个唯一实现类】
使用ExecutorService、Callable、Future实现有返回结果的线程
【Callable接口可看作Runnable接口的升级版,但是里面的方法不是run(),而是call()】
【通过Future对象可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。】
什么是线程安全
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
哪些集合类是线程安全的
Vector、Statck、HashTable、Enumeration
StringBuffer是线程安全,而StringBuilder是线程不安全的。
多线程中的忙循环是什么
忙循环就是程序员用循环让一个线程等待,不像传统方法wait(), sleep() 或 yield() 它们都放弃了CPU控制,而忙循环不会放弃CPU,它就是在运行一个空循环。这么做的目的是为了保留CPU缓存,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存和减少等待重建的时间就可以使用它了。(比如自旋锁)
什么是线程局部变量
ThreadLocal线程局部变量 高效地为每个使用它的线程提供单独的线程局部变量值的副本
ThreadLocalMap是ThreadLocal的一个内部类,不对外使用的。当使用ThreadLocal存值时,首先是获取到当前线程对象,然后获取到当前线程本地变量Map,最后将当前使用的ThreadLocal和传入的值放到Map中,也就是说ThreadLocalMap中存的值是[ThreadLocal对象, 存放的值],这样做的好处是,每个线程都对应一个本地变量的Map,所以一个线程可以存在多个线程本地变量。
ThreadLocal 常用方法是:void remove()、T get()、set(T) 方法
ThreadLocal 使用举例:Hiberante的Session 工具类HibernateUtil、通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性
ThreadLocal 原理?
ThreadLocal的实现是:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
补充:ThreadLocalMap 是 ThreadLocal 的static 内部类。
谈一谈弱引用,有什么实践经验?
弱引用(WeakReference):弱引用的对象比软引用的拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 实践经验: 在现实情况写代码的时候, 我们往往通过把所有指向某个对象的 referece 置空来保证这个对象在下次GC运行的时候被回收【如:myObj = null; 这样的语句】。 这样做可以让GC自动回收这块内存,但是一两个变量没问题,如果变量太多,岂不是很多麻烦的代码? 另外,如果使用缓存对象的话,虽然把对象的 referece 置为null,但是缓存中的引用是不会被GC的。 为了解决上述两个问题,我们可以直接使用弱引用数据类型(WeakReference、WeakHashMap),GC会回收之后没有使用到的 WeakReference 对象,而存在 WeakHashMap 中的key,只要后面没有使用到就会被GC清除回收内存。
ThreadLocal 中有使用弱引用吗,ThreadLocal 会有内存泄露情况吗?
ThreadLocal 中有使用弱引用,ThreadLocal 使用 set 方法设置值的时候,最终存入Entry对象中,而Entry对象 继承自 WeakReference。 ThreadLocal 会有内存泄露(参考 http://www.importnew.com/22039.html): 1、使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏; 2、分配使用了ThreadLocal又不再调用get、set、remove()方法,那么就会导致内存泄漏【get(),set(),remove()方法发现key为空时,会进行空间回收】
ThreadLocalMap 为什么 要 使用 弱引用作为key?
ThreadLocalMap 是使用ThreadLocal的弱引用作为key的。
由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。【补充,如果使用 ThreadLocal 存大对象,并且还没有及时remove,会造成内存溢出】
ThreadLocal 最佳实践
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
线程和进程有什么区别?进程间如何通讯
进程是资源分配的最小单位,线程是程序执行的最小单位。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。
传统的进程间通信的方式有大致如下几种: (1) 管道(PIPE) (2) 命名管道(FIFO) (3) 信号量(Semphore) (4) 消息队列(MessageQueue) (5) 共享内存(SharedMemory) (6) Socket 我们把Java进程理解为JVM进程,传统的这些大部分技术是无法被我们的应用程序利用了(这些进程间通信都是靠系统调用来实现的)。但是Java也有很多方法可以进行进程间通信的, 除了上面提到的Socket之外,当然首选的IPC可以使用RMI,当然还有RPC。
同步和异步有何异同,在什么情况下分别使用他们?举例说明
如果数据将在线程间共享。例如正在写的数据以后可能被另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就是共享数据,必须进行同步存取。
当应用程序在对象上调用了一个需要花费很长时间来执行的方法,并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。
Java中交互方式分为同步和异步两种:
同步交互:指发送一个请求,需要等待返回,然后才能够发送下一个请求,有个等待过程;
异步交互:指发送一个请求,不需要等待返回,随时可以再发送下一个请求,即不需要等待。
区别:一个需要等待,一个不需要等待,在部分情况下,我们的项目开发中都会优先选择不需要等待的异步交互方式。
哪些情况建议使用同步交互呢?比如银行的转账系统,对数据库的保存操作等等,都会使用同步交互操作,其余情况都优先使用异步交互
ArrayBlockingQueue, CountDownLatch的用法
两者都是 java.util.concurrent 包下的类 ArrayBlockingQueue:一个由数组支持的有界阻塞队列。它的本质是一个基于数组的BlockingQueue的实现。 它的容纳大小是固定的。此队列按 FIFO(先进先出)原则对元素进行排序。 ArrayBlockingQueue 是线程安全的,原因是 内部是通过 ReentrantLock 实现加锁的 不接受 null 元素 插入数据: (1)add(e)//队列未满时,返回true;队列满则抛出IllegalStateException (2)offer(e)//队列未满时,返回true;队列满时返回false;还有另外一个api:offer(e, time, unit) (3)put(e)//队列未满时,直接插入没有返回值;队列满时会阻塞等待,一直等到队列未满时再插入 队列元素的删除: (1)remove()//队列不为空时,返回队首值并移除;队列为空时抛出NoSuchElementException() (2)poll()//队列不为空时返回队首值并移除;队列为空时返回null。非阻塞立即返回,另外的api:poll(time, unit) (3)take(e)//队列不为空返回队首值并移除;当队列为空时会阻塞等待,一直等到队列不为空时再返回队首值。 size() 返回队列的长度 【 并发包中的其他阻塞队列: ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列; LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列,api 和 ArrayBlockingQueue 基本一致; PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列。 非阻塞队列: ConcurrentLinkedQueue: 入队(插入)使用 add(e),或者 offer(e)< 插入到队列尾部,add 方法内部调用的其实是offer >,入队的操作都是由CAS算法完成 出队(删除)使用 poll(),移除队首并返回元素,如果此队列为空,则返回 null。 那么对于栈呢:java.util.Stack 继承自 java.util.Vector,而 Vector 是List的子类 Stack 方法(除了List中的方法外的):push(e) 往栈中存入;peek() 获取栈顶元素(不出栈);pop() 获取栈顶元素且出栈(在peek 和 pop 时,如果没有对象,则异常 EmptyStackException) 】 CountDownLatch:用给定的计数初始化CountDownLath。调用countDown()方法计数减 1,在计数被减到 0之前,调用await方法会一直阻塞。减为 0之后,则会迅速释放所有阻塞等待的线程,并且调用await操作会立即返回。
ConcurrentHashMap的并发度是什么
ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap
CyclicBarrier 和 CountDownLatch有什么不同?各自的内部原理和用法是什么
CountDownLatch
一个线程活多个线程,等待另外一个线程或者多个线程完成某个事情之后才继续执行。
减计数方式
计算为0时释放所有等待的线程
计数为0时,无法重置
调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响
不可重复利用
关键方法:await()、countDown()
CyclicBarrier
多个线程之间互相等待,任何一个线程完成之前,所有线程都必须等待。
加计数方式
计数达到指定值时释放所有等待线程
计数达到指定值时,计数置为0重新开始
调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞
可重复利用
关键方法:await()、reset()
Semaphore的用法
Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。就这一点而言,单纯的synchronized 关键字是实现不了的。 Semaphore 需要多个线程使用同一个 Semaphore实例。 关键方法: semaphore.acquire(); // 申请进入 semaphore.release(); // 释放 在 semaphore.acquire() 和 semaphore.release()之间的代码,同一时刻只允许制定个数的线程进入【 制定个数 由初始化时作为参数传入 】 如果在 acquire 和 release 之间的代码是一个比较慢和复制的运算,如内存占用过多,或者栈深度很深等,jvm会中断这块代码,并抛出异常 InterruptedException acquire() 方法的扩展:tryAcquire() 、 tryAcquire(int permits)、 tryAcquire(int permits , long timeout , TimeUint unit) 参考:https://www.cnblogs.com/klbc/p/9500947.html
启动一个线程是调用 run() 还是 start() 方法?start() 和 run() 方法有什么区别
启动一个线程,应该调用的是start(),调用run()是在当前线程运行。
start()是真正的多线程,调用start()会新开一个线程,和main方法线程并发执行;调用run()则是在main方法中运行线程方法,并不是多线程。
sleep() 方法和对象的 wait() 方法都可以让线程暂停执行,它们有什么区别
sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复 wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lockpool),如果线程重新获得对象的锁就可以进入就绪状态
关于wait和notify,可以参考: https://www.cnblogs.com/YuyuanNo1/p/8004471.html
wait 和 notifyAll 可以实现消费者生产者,使用sleep 则不行(参考上一行链接)。
notify() 和 notifyAll() 的区别是什么?
以用 notify 和 notifyAll 来通知那些等待中的线程重新开始运行。不同之处在于,notify 仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll 会通知所有等待中的线程。
换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。 参考: https://www.cnblogs.com/YuyuanNo1/p/8004471.html
yield方法有什么作用?sleep() 方法和 yield() 方法有什么区别
1)sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会; 2)线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态; 3)sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常; 4)sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性.
Java 中如何停止一个线程
使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。 使用interrupt方法中断线程。 【补充: 弃用stop方法,原因是stop方法是直接kill线程,破坏了线程的原子性; interrupt()并不会终止处于“运行状态”的线程!它只会将线程的中断标记设为true。所以,必须配合 isInterrupted() 方法使用,在run()内部停止线程。 如: // main 方法中调用:t.interrupt(); public void run() { int i = 0; while (!Thread.currentThread().isInterrupted()) { System.out.println("thread is runing -- " + (++i)); } System.out.println("thread is Interrupted"); } 】
stop() 和 suspend() 方法为何不推荐使用
stop():不安全,它会解除由线程获取的所有锁定,当在一个线程对象上调用stop()方法时,这个线程对象所运行的线程就会立即停止,会破坏线程的原子性; suspend():容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被"挂起"的线程恢复运行。
join() 方法总结
t.join() 必须在 t.start() 运行之后,才有意义(二者调换顺序执行也是没有意义的)。 在A线程中调用了B线程的join()方法时,表示A线程让出cpu给B,直到 B运行完了之后,A线程才能继续执行。 join() 可以用来实现线程之间的同步。 其实,join方法是通过调用线程的wait方法来达到同步的目的的。
什么是线程组,为什么在Java中不推荐使用
ThreadGroup线程组表示一个线程的集合。此外,线程组也可以包含其他线程组。线程组构成一棵树,在树中,除了初始线程组外,每个线程组都有一个父线程组。 允许线程访问有关自己的线程组的信息,但是不允许它访问有关其线程组的父线程组或其他任何线程组的信息。线程组的目的就是对线程进行管理。 为什么不推荐使用 1.线程组ThreadGroup对象中比较有用的方法是stop、resume、suspend等方法,由于这几个方法会导致线程的安全问题(主要是死锁问题),已经被官方废弃掉了,所以线程组本身的应用价值就大打折扣了。 2.线程组ThreadGroup不是线程安全的,这在使用过程中获取的信息并不全是及时有效的,这就降低了它的统计使用价值。
你是如何调用 wait(方法的)?使用 if 块还是循环?为什么
wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好 可以参考 Thread类 的 join 方法 的源码
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
线程池是什么?为什么要使用它
降低资源消耗、提升响应速度、提高线程的可管理性
如何创建一个Java线程池
可以通过 java.util.concurrent.ThreadPoolExecutor 来创建一个线程池: new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds, runnableTaskQueue, threadFactory, handler); 参数说明: 1、corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。 2、maximumPoolSize(线程池最大数量):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是,如果使用了无界的任务队列这个参数就没用了。 3、keepAliveTime(线程活动时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大时间,提高线程利用率。 4、TimeUnit(线程活动时间的单位):可选的单位有天(Days)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒) 5、runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列: (1)ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,FIFO(先进先出)。 (2)LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue,静态工厂方法Executors.newFixedThreadPool()使用了这个队列。 (3)SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool()使用了这个队列。 (4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。 6、threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 7、RejectedExecutionHandler (饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略还处理新提交的任务。它可以有如下四个选项: AbortPolicy:直接抛出异常,默认情况下采用这种策略 CallerRunsPolicy:只用调用者所在线程来运行任务 DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务 DiscardPolicy:不处理,丢弃掉
corePoolSize 和 maximumPoolSize 的区别是啥?
关于 ThreadPoolExecutor 相关的任务线程,它包含两部分:正在线程池中运行的任务线程、在taskQueue 中排队等待运行的任务线程。 1、当线程池初始化完成之后,executorService.submit(new Thread(...)); 加入需要运行的任务线程,因为线程池初始化是没有线程运行的,所以当提交一个任务到线程池时,线程池会创建一个线程来执行任务; 2、当线程池中正在运行的线程达到 corePoolSize 个时,线程会放到 taskQueue 中排队等候; 3、当 taskQueue(阻塞队列)的容量达到上限(即队列中不能再加入任务线程了),而当前的poolSize(就是正在线程池中运行的任务线程个数)小于 maximumPoolSize 时,则新增线程来处理任务; 4、当 taskQueue 的容量达到上限,且 poolSize = maximumPoolSize,那么线程池已经达到极限,会根据饱和策略RejectedExecutionHandler拒绝新的任务。 像极了小时候做客的这个场景: 小朋友做客,桌上放了一桌的土豆条,小朋友有一个碗(taskQueue),碗里可以放10根土豆条(taskQueue容量 = 10),正常情况下,小朋友每次可以吃2根土豆条(嘴巴就是线程池,corePoolSize = 2),小朋友狼吞虎咽的吃每次可以吃4根土豆条(maximumPoolSize = 4)。 坐在餐桌边后,小朋友开始吃土豆,先往嘴里放了2根土豆条,但怕其他小朋友和自己抢,然后赶紧往自己的碗里加土豆条(加入队列),直到碗满了(现在小朋友嘴里2个土豆条,碗里10个土豆条)。小朋友依然害怕因为自己动作慢比其他人少吃,所以小朋友开始狼吞虎咽地吃(嘴里4个土豆条,碗里10个土豆条)。
newCache 和 newFixed 有什么区别?简述原理。构造函数的各个参数的含义是什么,比如 coreSize, maxsize 等
newSingleThreadExecutor 返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。
newFixedThreadPool 返回一个包含指定数目线程的线程池,如果任务数量多于线程数目,那么没有没有执行的任务必须等待,直到有任务完成为止。
newCachedThreadPool 根据用户的任务数创建相应的线程来处理,该线程池不会对线程数目加以限制,完全依赖于JVM能创建线程的数量,可能引起内存不足。
底层是基于ThreadPoolExecutor实现,借助reentrantlock保证并发。
coreSize核心线程数,maxsize最大线程数。
调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,二者区别
void shutdown() 停止接收外部submit的任务 内部正在跑的任务和队列里等待的任务,会执行完 List<Runnable> shutdownNow() 先停止接收外部提交的任务 忽略队列里等待的任务 尝试将正在跑的任务interrupt中断 返回未执行的任务列表 关闭功能 【从强到弱】 依次是:shuntdownNow() > shutdown() > awaitTermination()
为什么吞吐量 LinkedBlockingQueue 大于 ArrayBlockingQueue?
ArrayBlockingQueue 实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量
线程池的关闭方式有几种,各自的区别是什么
见上上个问题
ArrayBlockingQueue 和 LinkedBlockingQueue的区别?
1.队列中锁的实现不同 ArrayBlockingQueue实现的队列中的锁是没有分离的,即生产和消费用的是同一个锁; LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock; 2.在生产或消费时操作不同 ArrayBlockingQueue基于数组,在生产和消费的时候,是直接将枚举对象插入或移除的,不会产生或销毁任何额外的对象实例; LinkedBlockingQueue基于链表,在生产和消费的时候,需要把枚举对象转换为Node<E>进行插入或移除,会生成一个额外的Node对象,这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC的影响还是存在一定的区别; 3.队列大小初始化方式不同 ArrayBlockingQueue是有界的,必须指定队列的大小; LinkedBlockingQueue是无界的,可以不指定队列的大小,但是默认是Integer.MAX_VALUE。当然也可以指定队列大小,从而成为有界的; 注意: 1.在使用LinkedBlockingQueue时,若用默认大小且当生产速度大于消费速度时候,有可能会内存溢出; 2.在使用ArrayBlockingQueue和LinkedBlockingQueue分别对1000000个简单字符做入队操作时, LinkedBlockingQueue的消耗是ArrayBlockingQueue消耗的10倍左右, 即LinkedBlockingQueue消耗在1500ms左右,而ArrayBlockingQueue只需150ms左右。 性能测试:不限容量的LinkedBlockingQueue的吞吐量 > ArrayBlockingQueue > 限定容量的LinkedBlockingQueue
ConcurrentLinkedQueue 和 LinkedBlockingQueue的区别
Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 ConcurrentLinkedQueue 是非阻塞队列的代表,没有锁,使用CAS实现,有 add(e)、offer(e)、remove()、poll()和peek(),size() 是要遍历一遍集合的,速很慢,所以判空时,尽量要避免用size(),而改用isEmpty()。 LinkedBlockingQueue 是阻塞队列,内部则是基于锁,增加了 put(e)、take() 两个阻塞方法。 LinkedBlockingQueue 多用于任务队列,ConcurrentLinkedQueue 多用于消息队列 单生产者,单消费者 用 LinkedBlockingqueue 多生产者,单消费者 用 LinkedBlockingqueue 单生产者 ,多消费者 用 ConcurrentLinkedQueue 多生产者 ,多消费者 用 ConcurrentLinkedQueue 【 补充: java.util.concurrent包提供的容器(Queue、List、Set),Map,从名字上可以大概区分为Concurrent,CopyOnWrite,Blocking*等三类,同样是线程安全容器,可以简单认为: 1、Concurrent类型没有CopyOnWrite之类容器相对较重的修改开销; 2、但是,Concurrent提供了较低的遍历一致性,与弱一致性相对应的,就是同步容器常见的行为“fast-fail”,也就是检测到容器在遍历过程中发生了修改,则抛出“ConcurrentModificationException”,不再继续遍历; 3、弱一致性另外一个体现就是size等操作准确性是有限的,未必是100%正确; 】
CAS 和 AQS 简述
CAS(Compare And Swap),即比较并交换。
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。
CAS 的实现:AtomicInteger、AtomicBoolean、AtomicLong,里面的方法:getAndSet(newVal)【设置新值】、getAndIncrement()【自增】
AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架。
AQS 使用了一个原子的int status来作为同步器的状态(如:独占锁,1代表已占有,0代表未占有),通过该类提供的原子修改status方法(getState setState and compareAnsSetState),我们可以把它作为同步器的基础框架类来实现各种同步器。
AQS 的实现:CountDownLatch、CyclicBarrier、Semaphore(synchronized 的加强版,作用是控制线程的并发数量)、ReentrantLock(重入锁)、ReentrantReadWriteLock(读写锁)
线程池中submit() 和 execute()方法有什么区别?
execute(Runnable x) 没有返回值。可以执行任务,但无法判断任务是否成功完成。
submit(Runnable x) 返回一个future。可以用这个future来判断任务是否成功完成。
什么是多线程中的上下文切换
即使是单核CPU也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程时同时执行的,时间片一般是几十毫秒(ms)。
CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再次加载这个任务的状态,从任务保存到再加载的过程就是一次上下文切换。
这就像我们同时读两本书,当我们在读一本英文的技术书籍时,发现某个单词不认识,于是便打开中英文词典,但是在放下英文书籍之前,大脑必须先记住这本书读到了多少页的第多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文切换也会影响多线程的执行速度。
你对线程优先级的理解是什么
现代操作系统基本采用时分的形式调度运行的线程,线程分配得到的时间片的多少决定了线程使用处理器资源的多少,也对应了线程优先级这个概念。在JAVA线程中,通过一个int priority来控制优先级,范围为1-10,其中10最高,默认值为5 线程优先级有继承性,如果主线程启动threadA线程且threadA线程没有另外赋予优先级,则threadA线程优先级和main线程一样。优先级与执行顺序无关 CPU尽量将执行资源让给线程优先级高的,即线程优先级高的总是会大部分先执行,但是不代表高优先级的线程全部都先执行完再执行低优先级的线程 优先级具有随机性,优先级高的不一定每次都先执行
什么是线程调度器 (Thread Scheduler) 和时间分片 (Time Slicing)
线程调度器是一个操作系统服务,它负责为Runnable状态的线程分配CPU时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。时间分片是指将可用的CPU时间分配给可用的Runnable线程的过程。
分配CPU时间可以基于线程优先级或者线程等待的时间。线程调度并不受到Java虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
mysql 联合索引详解
联合索引又叫复合索引。对于复合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分。例如索引是key index (a,b,c)。 可以支持a | a,b| a,b,c 3种组合进行查找,但不支持 b,c进行查找 .当最左侧字段是常量引用时,索引就十分有效。 如 test 表中的复合索引(a,b,c) 查询优劣如下: 优: select * from test where a=10 and b>50 差: select * from test order by b 优: select * from test where a=10 order by b 优: select * from test where a=10 and b=10 order by c 优: select * from test where a=10 and b>10 order by b 差: select * from test where a=10 and b>10 order by c
请说出你所知的线程同步的方法
1、同步方法,synchronized关键字修饰的方法 2、同步代码块,有synchronized关键字修饰的语句块 3、wait与notify 4、使用特殊域变量(volatile)实现线程同步 5、使用重入锁实现线程同步(ReentrantLock) 6、使用局部变量实现线程同步(使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响) 7、使用阻塞队列(如 LinkedBlockingQueue)实现线程同步
synchronized 的原理是什么
synchronized 的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因 每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下: 1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。 2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1. 3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized 和 ReentrantLock 有什么不同
(1)ReentrantLock 拥有Synchronized相同的并发性和内存语义,此外还多了锁投票,定时锁等候和中断锁等候 (2)synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中 (3)在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态
什么场景下可以使用 volatile 替换 synchronized
volatile 可以保证 对象的可见性 和 程序的顺序性,无法保证操作的原子性。
volatile适用于新值不依赖于旧值的情形,比如:1写N读、单线程修改变量或不依赖当前值,且不与其他变量构成不变性条件时候使用
有T1,T2,T3三个线程,怎么确保它们按顺序执行?怎样保证T2在T1执行完后执行,T3在T2执行完后执行
使用join方法。三个线程同时start,在T3的run中,调用T2.join,让T2执行完成后再执行T3,在T2的run中,调用T1.join,让T1执行完成后再让T2执行
当一个线程进入一个对象的 synchronized 方法A 之后,其它线程是否可进入此对象的 synchronized 方法B
如果方法A内部调用了wait,则可以进入其他synchronized方法;
如果方法A内部没有调用wait,则不能进入其他synchronized方法;
如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this
使用 synchronized 修饰静态方法和非静态方法有什么区别
非静态方法是获取对象锁(如this)
静态方法是获取类锁 (如:Demo.class)
如何从给定集合那里创建一个 synchronized 的集合
我们可以使用 Collections.synchronizedCollection(Collection c)根据指定集合来获取一个synchronized(线程安全的)集合。 或者: List list = Collections.synchronizedList(new ArrayList()); Set set = Collections.synchronizedSet(new HashSet()); Map map = Collections.synchronizedMap(new HashMap());
ReadWriteLock是什么?
实现类ReentrantReadWriteLock,可重入的读写锁,允许多个读线程获得ReadLock,但只允许一个写线程获得WriteLock 读写锁的机制: "读-读" 不互斥 "读-写" 互斥 "写-写" 互斥
解释以下名词:重排序,自旋锁,偏向锁,轻量级锁,可重入锁,公平锁,非公平锁,乐观锁,悲观锁
指令重排序 是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序员带来问题。 volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。 自旋锁 自旋锁可以使线程在没有取得锁的时候,不被挂起,而转去执行一个空循环,(即所谓的自旋,就是自己执行空循环),若在若干个空循环后,线程如果可以获得锁,则继续执行。若线程依然不能获得锁,才会被挂起。 使用自旋锁后,线程被挂起的几率相对减少,线程执行的连贯性相对加强。因此,对于那些锁竞争不是很激烈,锁占用时间很短的并发线程,具有一定的积极意义,但对于锁竞争激烈,单线程锁占用很长时间的并发程序,自旋锁在自旋等待后,往往毅然无法获得对应的锁,不仅仅白白浪费了CPU时间,最终还是免不了被挂起的操作 ,反而浪费了系统的资源。 在JDK1.6中,Java虚拟机提供-XX:+UseSpinning参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。 在JDK1.7开始,自旋锁的参数被取消,虚拟机不再支持由用户配置自旋锁,自旋锁总是会执行,自旋锁次数也由虚拟机自动调整。 偏向锁 Java6引入的一项多线程优化,偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁)。 轻量级锁 “轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。在解释轻量级锁的执行过程之前,先明白一点,轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。 可重入锁 可重入锁,也叫做递归锁,指的是同一线程 外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。 在JAVA环境下 ReentrantLock 和synchronized 都是 可重入锁 公平锁 就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己 非公平锁 比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。 乐观锁 乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作。 java中的乐观锁基本都是通过CAS操作实现的,CAS是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。 悲观锁 悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如RetreenLock。
为什么要使用消息队列?
原因有3个: 解耦:传统的方式是多个系统之间调用,多个系统之间耦合关系太强,每当某个系统有修改后(或者新系统接入),其他系统也需要对应进行修改,而使用消息队列则可以降低耦合性 异步:传统系统中,一些非必须的业务逻辑也使用同步进行操作,导致运行时间过长;使用消息队列,把这些逻辑放入队列处理,可以较快地获得返回 削峰:成千上万个用户访问系统时,对数据库的压力会比较大,使用中间件,可以根据数据库的处理能力,从消息队列中取数据,从而避免了数据库挂掉 参考:http://www.cnblogs.com/williamjie/p/9481780.html
使用了消息队列会有什么缺点?
1、系统可用性降低:之前是两个系统之间直接调用,现在加了一个中间件,相当于多了一个步骤,而且万一中间件挂了,那么系统运行就存在问题了。 2、系统复杂性增加:加入中间件后,系统需要考虑的东西增加,如何保证消息的一致性、如何保证消息不被重复消费、如何保证消息的可靠传输。
消息队列如何选型?
基本比较: ActiveMQ:开发语言java,单机吞吐量万级,时效性ms级,可用性高(主从架构) RabbitMQ:开发语言erlang,单机吞吐量万级,时效性us级,可用性高(主从架构) RocketMQ:开发语言java,单机吞吐量10万级,时效性ms级,可用性非常高(分布式架构) kafka:开发语言scala,单机吞吐量10万级,时效性ms级以内,可用性非常高(分布式架构) 功能特性比较: ActiveMQ:成熟的产品,在很多公司得到应用;有较多的文档;各种协议支持较好 RabbitMQ:基于erlang开发,所以并发能力很强,性能极其好,延时很低;管理界面较丰富 RocketMQ:MQ功能比较完备,扩展性佳 kafka:只支持主要的MQ功能,像一些消息查询,消息回溯等功能没有提供,毕竟是为大数据准备的,在大数据领域应用广。 结论: (1)中小型软件公司,建议选RabbitMQ.一方面,erlang语言天生具备高并发的特性,而且他的管理界面用起来十分方便。正所谓,成也萧何,败也萧何!他的弊端也在这里,虽然RabbitMQ是开源的,然而国内有几个能定制化开发erlang的程序员呢?所幸,RabbitMQ的社区十分活跃,可以解决开发过程中遇到的bug,这点对于中小型公司来说十分重要。不考虑rocketmq和kafka的原因是,一方面中小型软件公司不如互联网公司,数据量没那么大,选消息中间件,应首选功能比较完备的,所以kafka排除。不考虑rocketmq的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。 (2)大型软件公司,根据具体使用在rocketMq和kafka之间二选一。一方面,大型软件公司,具备足够的资金搭建分布式环境,也具备足够大的数据量。针对rocketMQ,大型软件公司也可以抽出人手对rocketMQ进行定制化开发,毕竟国内有能力改JAVA源码的人,还是相当多的。至于kafka,根据业务场景选择,如果有日志采集功能,肯定是首选kafka了。具体该选哪个,看使用场景。 个人觉得中小型企业,也可以选择ActiveMQ的,因为ActiveMQ是一个比较成熟的产品(最老的MQ),功能也很齐全,唯一不足的就是版本更新太慢(官方维护少)。
如何保证消息消费的顺序性?
如果存在多个消费者,那么就让每个消费者对应一个queue,然后把要发送 的数据全都放到一个queue,这样就能保证所有的数据只到达一个消费者从而保证每个数据到达数据库都是顺序的。
拆分多个queue,每个queue一个consumer,就是多一些queue而已,确实是麻烦点;或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理
参考: https://juejin.im/post/5c9b1c155188251d806727b2
MQ积压几百万条数据怎么办?
一般这个时候,只能操作临时紧急扩容了,具体操作步骤和思路如下:
1、先修复consumer的问题,确保其恢复消费速度,然后将现有consumer都停掉 2、新建一个topic,partition是原来的10倍,临时建立好原先10倍或者20倍的queue数量 3、然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue 4、接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据 5、这种做法相当于是临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据 6、等快速消费完积压数据之后,得恢复原先部署架构,重新用原先的consumer机器来消费消息
如何保证消息不被重复消费?
造成重复消费的原因?,就是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将该消息分发给其他的消费者。 如何解决?这个问题针对业务场景来答分以下几点 (1)比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。 (2)再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。 (3)如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。
如何保证消费的可靠性传输?
可靠性传输,每种MQ都要从三个角度来分析:生产者弄丢数据、消息队列弄丢数据、消费者弄丢数据 对于RabbitMQ: (1)生产者丢数据:RabbitMQ提供 transaction 和 confirm模式 来确保生产者不丢消息 transaction 机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。缺点就是吞吐量下降了。 生产上用 confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。 (2)处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。 那么如何持久化呢,就下面两步:1、将queue的持久化标识durable设置为true,则代表是一个持久的队列;2、发送消息的时候将deliveryMode=2 (3)消费者丢数据:消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时rahbitMQ会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。至于解决方案,采用手动确认消息即可。 对于kafka: Producer在发布消息到某个Partition时,先通过ZooKeeper找到该Partition的Leader,然后无论该Topic的Replication Factor为多少(也即该Partition有多少个Replication),Producer只将该消息发送到该Partition的Leader。Leader会将该消息写入其本地Log。每个Follower都从Leader中pull数据。 (1)生产者丢数据:在kafka生产中,基本都有一个leader和多个follwer。follwer会去同步leader的信息。因此,为了避免生产者丢数据,做如下两点配置: 1)在producer端设置acks=all。这个配置保证了,follwer同步完成后,才认为消息发送成功。 2)在producer端设置retries=MAX,一旦写入失败,这无限重试 (2)消息队列弄丢数据:针对消息队列丢数据的情况,无外乎就是,数据还没同步,leader就挂了,这时zookpeer会将其他的follwer切换为leader,那数据就丢失了。针对这种情况,应该做两个配置。 1)replication.factor参数,这个值必须大于1,即要求每个partition必须有至少2个副本 2)min.insync.replicas参数,这个值必须大于1,这个是要求一个leader至少感知到有至少一个follower还跟自己保持联系 这两个配置加上上面生产者的配置联合起来用,基本可确保kafka不丢数据 (3)消费者丢数据:这种情况一般是自动提交了offset,然后你处理程序过程中挂了。kafka以为你处理好了。解决方案也很简单,改成手动提交即可。 (补充:offset:指的是kafka的topic中的每个消费组消费的下标。简单的来说就是一条消息对应一个offset下标,每次消费数据的时候如果提交offset,那么下次消费就会从提交的offset加一那里开始消费。比如一个topic中有100条数据,我消费了50条并且提交了,那么此时的kafka服务端记录提交的offset就是49(offset从0开始),那么下次消费的时候offset就从50开始消费。) 对于ActiveMQ: (1)生产者丢数据:可以使用同步发送消息:org.apache.activemq.ActiveMQConnectionFactory中引入: <property name="useAsyncSend" value="true" /> <!-- false:同步 true:异步 --> (2)消息队列弄丢数据:对于Queue而言,支持 storeCursor(消息存到storeEngine中)、vmQueueCursor(消息存于内存)、fileQueueCursor(消息存到临时文件中)。其中storeCursor是一个“综合”策略,持久化消息使用fileQueueCurosr支持,非持久化消息使用vmQueueCursor支持。 对应的 activemq.xml 下的配置是:pendingQueuePolicy(PendingQueueMessageStoragePolicy : 待消息转存策略) (3)消费者丢数据:使用消息事务(吞吐量不高,去掉),或者消息应答模式(acknowledge model)改为客户端手动确认 确认机制(ack_mod): Session.AUTO_ACKNOWLEDGE 自动确认;Session.CLIENT_ACKNOWLEDGE 客户端手动确认;Session.DUPS_OK_ACKNOWLEDGE 自动批量确认;Session.SESSION_TRANSACTED 事务提交并确认
对于RocketMq:
(1)生产者丢数据:可以通过同步的方式阻塞式的发送(默认情况),check SendStatus,状态是OK,表示消息一定成功的投递到了Broker,状态超时或者失败,则会触发默认的2次重试。采取事务消息的投递方式,并不能保证消息100%投递成功到了Broker,但是如果消息发送Ack失败的话,此消息会存储在CommitLog当中,后续可从CommitLog日志中获取相关消息。
(2)消息队列弄丢数据:消息持久化到日志文件,支持同步刷盘和异步刷盘,采用一主多从的复制模式,支持同步复制和异步复制
(3)消费者丢数据:配置consumer的 offset 持久化,集群模式,定时持久化到remote name server;广播模式,offset定时持久化到local file
{ RocketMq 配置consumer的 offset 持久化:
1、Consumer自身维护一个持久化的offset(对应MessageQueue里面的min offset),标记已经成功消费或者已经成功发回到broker的消息下标(定时调用RemoteBrokerOffsetStore.persistAll 更新到broker;每次pull_message时 上传的commitOffset,来自于本地offsetTable)
2、如果Consumer消费失败,那么它会把这个消息发回给Broker,发回成功后,再更新自己的offset
3、如果Consumer消费失败,发回给broker时,broker挂掉了,那么Consumer会定时重试这个操作
4、如果Consumer和broker一起挂了,消息也不会丢失,因为consumer 里面的offset是定时持久化的,重启之后,继续拉取offset之前的消息到本地
消费进度(OffsetStore)实现类2个:LocalFileOffsetStore、RemoteBrokerOffsetStore
}
简述锁的等级方法锁、对象锁、类锁
方法锁(synchronized修饰方法时):每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞
对象锁(synchronized修饰方法或代码块):当一个对象中有synchronized 方法 或 synchronized block 的时候调用此对象的同步方法或进入其同步区域时,就必须先获得对象锁
类锁(synchronized修饰静态的方法或代码块):由于一个class不论被实例化多少次,其中的静态方法和静态变量在内存中都只有一份。故一旦一个静态的方法被申明为synchronized,则此类所有的实例化对象在调用此方法,共用同一把锁,我们称之为类锁
个人理解:对象锁 是 方法锁的父集
Java中活锁和死锁有什么区别?
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。 死锁发生的四个条件: 1、互斥条件:线程对资源的访问是排他性的,如果一个线程对占用了某资源,那么其他线程必须处于等待状态,直到资源被释放。 2、请求和保持条件:线程T1至少已经保持了一个资源R1占用,但又提出对另一个资源R2请求,而此时,资源R2被其他线程T2占用,于是该线程T1也必须等待,但又对自己保持的资源R1不释放。 3、不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程剥夺,只能在使用完以后由自己释放。 4、环路等待条件:在死锁发生时,必然存在一个“进程-资源环形链”,进程p0(或线程)等待p1占用的资源,p1等待p2占用的资源,pn等待p0占用的资源。 活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。 补充: 饥饿:(低优先级)线程一直得不到运行 死锁进程等待永远不会被释放的资源,饿死进程等待会被释放但却不会分配给自己的资源 死锁一定涉及多个进程,而饥饿或被饿死的进程可能只有一个。在饥饿的情形下,系统中有至少一个进程能正常运行,只是饥饿进程得不到执行机会
怎么检测一个线程是否拥有锁
java.lang.Thread中有一个方法叫holdsLock(),它返回true如果当且仅当当前线程拥有某个具体对象的锁
Executors类是什么? Executor和Executors的区别
Executor 是接口对象,执行我们的线程任务,实现类是ThreadPoolExecutor ;
Executors 是工具类,不同方法按照我们的需求创建了不同的线程池,来满足业务的需求。
有哪些无锁数据结构,他们实现的原理是什么
无锁数据结构的实现主要基于两个方面:原子性操作和内存访问控制方法 java 1.5提供了一种无锁队列(wait-free/lock-free)ConcurrentLinkedQueue,可支持多个生产者多个消费者线程的环境 ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全队列,它采用先进先出的规则对节点进行排序,它采用了 CAS 算法来实现
如何在Java中获取线程堆栈
Java虚拟机提供了线程转储(thread dump)的后门,通过这个后门可以把线程堆栈打印出来。 jdk自带的打印线程堆栈的工具:jstack 示例:jstack –l 23561 >> xxx.dump
说出 3 条在 Java 中使用线程的最佳实践
(1)给你的线程起个有意义的名字,这样可以方便找bug或追踪。 如:OrderProcessor, QuoteProcessor or TradeProcessor这种名字比Thread-1. Thread-2 and Thread-3好多了。 主要框架甚至JDK都遵循这个最佳实践。 (2)最低限度的使用同步和锁,缩小临界区。 因此相对于同步方法我更喜欢同步块,它给我拥有对锁的绝对控制权。 (3)多用同步类少用wait和notify。CountDownLatch, Semaphore, CyclicBarrier和Exchanger这些同步类简化了编码操作,而用wait和notify很难实现对复杂控制流的控制。 (4)多用并发集合少用同步集合。 并发集合比同步集合的可扩展性更好,所以在并发编程时使用并发集合效果更好。如果下一次你需要用到map,你应该首先想到用ConcurrentHashMap。
在线程中你怎么处理不可捕捉异常
为了保证主线程不被阻塞,线程之间基本相互隔离,所以线程之间不论是异常还是通信都不共享。当然,因为你抓异常是主线程,而异常是在子线程出现,可以用thread.setUncaughtExceptionHandler()去处理线程的异常。
请说出与线程同步以及线程调度相关的方法
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁; sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常; notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关; notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态; 补充:Java 5通过Lock接口提供了显式的锁机制(explicit lock),增强了灵活性以及对线程的协调。Lock接口中定义了加锁(lock())和解锁(unlock())的方法,同时还提供了newCondition()方法来产生用于线程之间通信的Condition对象;
此外,Java 5还提供了信号量机制(semaphore),信号量可以用来限制对某个共享资源进行访问的线程的数量。在对资源进行访问之前,线程必须得到信号量的许可(调用Semaphore对象的acquire()方法);
在完成对资源的访问后,线程必须向信号量归还许可(调用Semaphore对象的release()方法)。
如何确保 main() 方法所在的线程是 Java 程序最后结束的线程
我们可以使用Thread类的join()方法来确保所有程序创建的线程在main()方法退出前结束。
Error 和 Exception有什么区别
Error类一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢出等。如java.lang.StackOverFlowError和Java.lang.OutOfMemoryError。
对于这类错误,Java编译器不去检查他们。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和预防,遇到这样的错误,建议让程序终止。
Exception类表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
UnsupportedOperationException是什么
java.lang.UnsupportedOperationException是不支持功能异常。 常常出现在使用Arrays.asList()后调用add,remove这些方法时。 原因是: Arrays.asList() 返回java.util.Arrays$ArrayList, 而不是ArrayList。Arrays$ArrayList和ArrayList都是继承AbstractList,remove,add等 method在AbstractList中是默认throw UnsupportedOperationException而且不作任何操作。
ArrayList override这些方法来对list进行操作,但是Arrays$ArrayList没有override remove(int),add(int)等,所以throw UnsupportedOperationException。 解决上面的问题只需要把list再放进java.util.ArrayList中就行了,List list2=new ArrayList(list),然后就可以用lists来做各种操作了。 参考: https://blog.csdn.net/dwmul/article/details/84258273
什么是受检查的异常,什么是运行时异常
Java提供了两类主要的异常 :RuntimeException 和 checked exception。
RuntimeException 就是运行时异常,我们可以不处理,当出现这样的异常时,总是由虚拟机接管。比如:我们从来没有人去处理过 NullPointerException 异常,它就是运行时异常。
checked异常也就是我们经常遇到的IO异常,以及SQL异常都是这种异常。对于这种异常,JAVA编译器强制要求我们必需对出现的这些异常进行catch。
运行时异常与一般异常有何异同
运行时异常表示虚拟机的通常操作中可能遇到的异常,是一种常见运行错误。java 编译器要求方法必须声明抛出可能发生的非运行时异常,但是并不要求必须声明抛出未被捕获的运行时异常。
简述一个你最常见到的runtime exception(运行时异常)
当试图将对象强制转换为不是实例的子类时,抛出ClassCastException 一个整数“除以零”时,抛出ArithmeticException异常 当应用程序试图在需要对象的地方使用 null 时,抛出NullPointerException异常 指示索引或者为负,或者超出字符串的大小,抛出StringIndexOutOfBoundsException异常 如果应用程序试图创建大小为负的数组,则抛出NegativeArraySizeException异常。
如果执行finally代码块之前方法返回了结果,或者JVM退出了,finally块中的代码还会执行吗
如果在 try块 之前就方法返回,则finally块中代码不会执行;如果是在 try块 中方法返回,finally里的代码会继续执行;JVM退出(如 System.exit(0);),则finally块中的代码不会执行
throw 和 throws 有什么区别?
throws 总是出现在一个函数头中,用来标明该成员函数可能抛出的各种异常,但这不是编译器强制的。如果方法抛出了异常那么调用这个方法的时候就需要将这个异常处理。
throw 是用来抛出任意异常的,按照语法你可以抛出任意 Throwable,throw可以中断程序运行,因此可以用来代替return。
既然我们可以用RuntimeException来处理错误,那么你认为为什么Java中还存在检查型异常
这是一个有争议的问题,在回答该问题时你应当小心。
其中一个理由是,存在检查型异常是一个设计上的决定,受到了诸如C++等比Java更早的编程语言设计经验的影响。Java 确保了你能够优雅的对异常进行处理。
通过 JDBC 连接数据库有哪几种方式
DriverManager、DataSource子类、DBCP、c3p0
JDBC 中如何进行事务处理
Connection提供了事务处理的方法,通过调用setAutoCommit(false)可以设置手动提交事务;当事务完成后用commit()显式提交事务;如果在事务处理过程中发生异常则通过rollback()进行事务回滚。
除此之外,从JDBC 3.0中还引入了Savepoint(保存点)的概念,允许通过代码设置保存点并让事务回滚到指定的保存点。
什么是 JdbcTemplate
Spring使用模板方式封装jdbc数据库操作固定流程,并提供丰富callback回调接口功能,方便用户自定义加工细节,更好模块化jdbc操作,简化传统的JDBC操作的复杂和繁琐过程。 (1) execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句; (2) update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句; (3) query方法及queryForXXX方法:用于执行查询相关语句; (4) call方法:用于执行存储过程、函数相关语句。
什么是 DAO 模块
DAO(DataAccess Object)顾名思义是一个为数据库或其他持久化机制提供了抽象接口的对象,在不暴露数据库实现细节的前提下提供了各种数据操作。
列出 5 个应该遵循的 JDBC 最佳实践
1. 使用PrearedStatement 来避免 SQL 异常(可以防止SQL注入) 2. 使用ConnectionPool(连接池) 3. 禁用自动提交(事务) 4. 使用Batch Update,可以减少数据库数据传输的往返次数,从而提高性能 5. 使用变量绑定而不是字符串拼接 6. 尽量使用标准的SQL语句,从而在某种程度上避免数据库对SQL支持的差异
File类型中定义了什么方法来创建一级目录
java.io.File.mkdir():只能创建一级目录,且父目录必须存在,否则无法成功创建一个目录。 java.io.File.mkdirs():可以创建多级目录,父目录不一定存在。
为了提高读写性能,可以采用什么流
缓冲流
Java中有几种类型的流
字节流,字符流。字节流继承于InputStream OutputStream,字符流继承于InputStreamReader OutputStreamWriter。在java.io包中还有许多其他的流,主要是为了提高性能和使用方便。
JDK 为每种类型的流提供了一些抽象类以供继承,分别是哪些类
字节流继承于InputStream OutputStream,
字符流继承于InputStreamReader OutputStreamWriter。
对文本文件操作用什么I/O流
FileReader、FileWriter
对各种基本数据类型和String类型的读写,采用什么流
DataInputStream、DataOutputStream
能指定字符编码的 I/O 流类型是什么
InputStreamReader、OutputStreamWriter
什么是序列化?如何实现 Java 序列化及注意事项
序列化:将一个对象编码成一个字节流,通过保存或传输这些字节流数据来达到数据持久化的目的; 反序列化:将字节流转换成一个对象; 实现方式: 1.对象实现了序列化接口Serializable 2.实现接口Externalizable Externlizable接口继承了java的序列化接口,并增加了两个方法: void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; 3.其它方式: 是把对象包装成JSON字符串传输。比如使用Google的gson-2.2.2.jar 进行转义 采用谷歌的ProtoBuf 注意事项: 1、如果子类实现Serializable接口而父类未实现时,父类不会被序列化,但此时父类必须有个无参构造方法,否则会抛InvalidClassException异常。 2、静态变量不会被序列化,那是类的“菜”,不是对象的。 3、transient关键字修饰变量可以限制序列化。 4、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致,就是 private static final long serialVersionUID = 1L。 5、Java 序列化机制为了节省磁盘空间,具有特定的存储规则,当写入文件的为同一对象时,并不会再将对象的内容进行存储,而只是再次存储一份引用。反序列化时,恢复引用关系。 6、序列化到同一个文件时,如第二次修改了相同对象属性值再次保存时候,虚拟机根据引用关系知道已经有一个相同对象已经写入文件,因此只保存第二次写的引用,所以读取时,都是第一次保存的对象。读者在使用一个文件多次 writeObject 需要特别注意这个问题(基于第5点)。
Serializable 与 Externalizable 的区别
Serializable:一个对象想要被序列化,那么它的类就要实现 此接口,这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递;
Externalizable:是Serializable接口的子接口,有时我们不希望序列化那么多,可以使用这个接口,这个接口的writeExternal()和readExternal()方法可以指定序列化哪些属性。
socket 选项 TCP NO DELAY 是指什么
public boolean getTcpNoDelay() throws SocketException public void setTcpNoDelay(boolean on) throws SocketException 在默认情况下,客户端向服务器发送数据时,会根据数据包的大小决定是否立即发送。当数据包中的数据很少时,如只有1个字节,而数据包的头却有几十个字节(IP头+TCP头)时,系统会在发送之前先将较小的包合并到软大的包后,一起将数据发送出去。在发送下一个数据包时,系统会等待服务器对前一个数据包的响应,当收到服务器的响应后,再发送下一个数据包,这就是所谓的Nagle算法;在默认情况下,Nagle算法是开启的。这种算法虽然可以有效地改善网络传输的效率,但对于网络速度比较慢,而且对实现性的要求比较高的情况下(如游戏、Telnet等),使用这种方式传输数据 会使得客户端有明显的停顿现象。因此,最好的解决方案就是需要Nagle算法时就使用它,不需要时就关闭它。而使用setTcpToDelay正好可以满 足这个需求。当使用setTcpNoDelay(true)将Nagle算法关闭后,客户端每发送一次数据,无论数据包的大小都会将这些数据发送出去。
Socket 工作在 TCP/IP 协议栈是哪一层
Socket是一组编程接口(API)。介于传输层和应用层,向应用层提供统一的编程接口。应用层不必了解TCP/IP协议细节。直接通过对Socket接口函数的调用完成数据在IP网络的传输。
TCP、UDP 区别及 Java 实现方式
TCP---传输控制协议,提供的是面向连接、可靠的字节流服务。当客户和服务器彼此交换数据前,必须先在双方之间建立一个TCP连接,之后才能传输数据。TCP提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端。 UDP---用户数据报协议,是一个简单的面向数据报的运输层协议。UDP不提供可靠性,它只是把应用程序传给IP层的数据报发送出去,但是并不能保证它们能到达目的地。由于UDP在传输数据报前不用在客户和服务器之间建立一个连接,且没有超时重发等机制,故而传输速度很快 Java 通过Socket实现TCP 和 UDP
直接缓冲区与非直接缓冲器有什么区别?
非直接缓冲区:通过allocate()分配缓冲区,将缓冲区建立在JVM的内存中
直接缓冲区:通过allocateDirect()分配直接缓冲区,将缓冲区建立在物理内存中,可以提高效率。
怎么读写 ByteBuffer?ByteBuffer 中的字节序是什么
ByteBuffer是NIO里用得最多的Buffer,它包含两个实现方式:HeapByteBuffer是基于Java堆的实现,而DirectByteBuffer则使用了unsafe的API进行了堆外的实现。 put(byte)和get()。分别是往ByteBuffer里写一个字节,和读一个字节。
当用System.in.read(buffer)从键盘输入一行n个字符后,存储在缓冲区buffer中的字节数是多少
存储在缓冲区buffer中的字节数有n+2个,即除输入的n个字符后,还存储了回车和换行字符。
解释下多态性(polymorphism),封装性(encapsulation),内聚(cohesion)以及耦合(coupling)
多态:方法的重载(Overload)和覆盖(Override)
内聚:设计某个模块或者关注点时,模块或关注点内部的一系列相关功能的相关程度的高低。高内聚提供了更好的可维护性和可复用性。而低内聚的模块则表明模块直接的依赖程度高,那么一旦修改了该模块依赖的对象则无法使用该模块,必须也进行相应的修改才可以继续使用。
耦合:软件工程中对象之间的耦合度就是对象之间的依赖性。
对象封装的原则是什么?
简化用户接口,隐藏实现细节,这个是封装的根本目的。 1、必须保证接口是功能的全集,即接口能够覆盖所有需求。 不能完成必要功能的封装是没有意义的; 2、尽量使接口是最小冗余的(单一职责原则); 3、要保证接口是稳定的,将接口和实现分离,并将实现隐藏(依赖倒置原则,要面向接口编程); 4、一旦接口被公布,永远也不要改变它(开闭原则,对扩展开发,对修改关闭)
反射中 Class.forName 和 ClassLoader 区别
java中class.forName()和classLoader都可用来对类进行加载。
class.forName()前者除了将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块。
而classLoader只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。
反射创建类实例的三种方式是什么
(1)方式一:Object类中的getClass()方法的。想要用这种方式,必须要明确具体的类,并创建对象。麻烦。 (2)方式二:任何数据类型(基本数据类型和引用数据类型)都具备一个静态的属性.class来获取其对应的Class对象。相对简单,但是还是要明确用到类中的静态成员。还是不够扩展。 (3)方式三:只要通过给定的类的 字符串名称就可以获取该类,更为扩展。可是用Class.forName的方法完成。这种方式只要有名称即可,更为方便,扩展性更强。
如何让前端 js 无法使用 cookie,保证网址安全?
有以下两种办法: 1、修改nginx,在 server模块 下面加入以下内容 : add_header Set-Cookie "HttpOnly"; # 在Cookie中设置了"HttpOnly"属性,通过程序(JS、Applet等)将无法读取到Cookie add_header Set-Cookie "Secure"; # 指示浏览器仅通过 HTTPS 连接传回 cookie add_header X-Frame-Options "SAMEORIGIN"; # 不允许一个页面在 <frame>, </iframe> 或者 <object> 中展现的标记 2、java后台 : response.addHeader("Set-Cookie", "mycookie=112; Path=/; HttpOnly"); //设置cookie response.addHeader("Set-Cookie", "mycookie=112; Path=/; Secure; HttpOnly"); //设置https的cookie response.setHeader("x-frame-options", "SAMEORIGIN"); //设置x-frame-options
mybatis的缓存机制
mybaits提供一级缓存,和二级缓存。 1、一级缓存: 一级缓存的作用域是同一个SqlSession(不同的sqlSession之间的缓存数据是互相不影响的),在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),
第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。 Mybatis默认开启一级缓存。 2、二级缓存; 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession去操作数据库得到数据会存在二级缓存区域,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。 Mybatis默认没有开启二级缓存需要在setting全局参数中配置开启二级缓存。 mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存。 3、刷新缓存: 如果sqlSession去执行commit操作(执行插入、更新、删除),则清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
如何通过反射获取和设置对象私有字段的值
可以通过类对象的getDeclaredField()方法获取字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
什么时候使用享元模式
统有大量相似对象。 需要缓冲池的场景。
如:JAVA中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。数据库的数据池。
缓存穿透,缓存击穿,缓存雪崩解决方案分析
1、缓存穿透:前端向后端查询一个不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,导致每次查询都需要从数据库中查询,缓存失去意义。流量大是容易造成DB挂掉,存在被人利用和进行攻击的可能。 方案:如果查询的数据不存在,则将数据进行空缓存,可将缓存的时间调短一点,比如5分钟。 2、缓存击穿:对于一些即将过期的但比较热门的key,如果过期的一瞬间,很多请求进来,就会造成DB的很大的负担。 方案:使用互斥锁,在获取数据的时候,新建一个短时间的key作为锁,获取到锁的请求,去DB查询数据,并写入缓存,同时删除这个锁,而其他请求的线程则自旋(线程循环等待)等待。 3、缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。 方案:在设置缓存时,将缓存的过期时间分散,可以在原定时间基础上加上一个随机时间,比如过期时间加上 1~5分钟 中的一个随机时间。
哪种依赖注入方式你建议使用,构造器注入,还是 Setter方法注入
Setter
setter方法设定依赖关系显得更加直观,更加自然。
如果依赖关系(或继承关系)较为复杂,那么构造函数也会相当庞大(我们需要在构造函数中设定所有依赖关系)
对于某些第三方类库而言,可能要求我们的组件必须提供一个默认的构造函数(如Struts中的Action),此时构造的依赖注入机制就体现出其局限性,难以完成我们期望的功能。
Redis分布式锁的正确实现方式
代码如下: private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; // 尝试获取 redis 分布式锁 public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) { String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; } 说明: 加锁的 关键代码是 :jedis.set(String key, String value, String nxxx, String expx, int time) 1、这个语句是原子的,就算系统挂了,锁只会要么加上要么没加上,不会有两个线程同时获得锁; 2、value 为 requestId(有key作为锁不就够了吗,为什么还要用到value?---> 因为设置requestId后,就知道这个锁是哪个线程加的,解开锁也必须这个线程才行),可以使用 java 的UUID; 3、只有key不存在时才加,存在了则不做任何操作; 4、设置了过期时间,不会造成线程死锁。
Zookeeper的分布式锁实现方式
1、客户端连接zookeeper,并在/lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-0000000000,第二个为/lock/lock-0000000001,以此类推。 2、客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听/lock的子节点变更消息,获得子节点变更通知后重复此步骤直至获得锁。 3、执行业务代码。 4、完成业务逻辑,删除对应的子节点释放锁。 (如果某个线程在创建临时节点后,服务挂了,因为zookeeper有定时的心跳机制,检测到线程连接断开,就会删除此线程创建的临时节点) 这个实现不足: 当lock下面的子节点变化后,所有线程都会获取/lock 下所有子节点,并循环,判断当前节点是否是最小的节点,这个会造成很多不必要的消耗。 改进: 每个节点的创建者只需要关注比自己序号小的那个节点,每个节点所关心的节点只有一个。 改进中存在的问题: 如果比自己序号小的哪个节点是因为网络挂了断开的连接,那么岂不是当前线程直接获取到锁了吗? 错,获取锁的时候,还是步骤2 的算法,获取 /lock 下的子节点列表,判断自己的节点是否是最小的。这不过这个时候,只有这一个线程被唤醒了,其他线程还是继续等待。
Zookeeper的分布式锁实现方式(二)
其实上面的算法已经有实现封装了,不用自己写代码了。直接拿来用就可以了。 可以使用 menagerie 来实现以上分布式锁,menagerie基于Zookeeper实现了java.util.concurrent 包的一个分布式版本。 github地址:https://github.com/sfines/menagerie 这个封装是更大粒度上对各种分布式一致性使用场景的抽象。其中最基础和常用的是一个分布式锁的实现: org.menagerie.locks.ReentrantZkLock
请思考一个方案,实现分布式环境下的 CountDownLatch
使用zookeeper实现(可参考 org.menagerie.latches.ZkCountDownLatch )、或者使用缓存实现
如何判断链表中是否有环?
可以使用哈希缓存,也可以使用快慢指针。
说出数据连接池的工作机制是什么
数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。
连接池的最大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。 数据库连接池的最小连接数和最大连接数的设置要考虑到下列几个因素: 1) 最小连接数是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费; 2) 最大连接数是连接池能申请的最大连接数,如果数据库连接请求超过此数,后面的数据库连接请求将被加入到等待队列中,这会影响之后的数据库操作。 3) 如果最小连接数与最大连接数相差太大,那么最先的连接请求将会获利,之后超过最小连接数量的连接请求等价于建立一个新的数据库连接。不过,这些大于最小连接数的数据库连接在使用完不会马上被释放,它将被放到连接池中等待重复使用或是空闲超时后被释放。
如何搭建一个高可用系统
1,主备/集群模式,防止单点 2,限流,削峰,防止后端压力过大 3,熔断机制,类似与限流 4,容灾机制,多机房/异地部署 减少单点 – 多点的地方很多,如机房(同城异地双机房),应用服务器,DNS服务器,SFTP服务器,LBS,缓存服务器,数据库,消息服务器,代理服务器和专线等,如系统通过专线调用对方服务,需要考虑同时拉联通和电信的专线。优先使用软负载,使用硬负载兜底。 减少依赖 – 减少DNS依赖,减少远程服务依赖,DNS依赖可以尝试设置本地host,用工具给所有服务器推送最新的域名映射关系,通过本地缓存或近端服务减少RPC调用。 限制循环 – 避免无限死循环,导致CPU利用率百分百,可以设置for循环的最大循环次数,如最大循环1000次。 控制流量 – 避免异常流量对应用服务器产生影响,可以对指定服务设置流量限制,如QPS,TPS TPS:Transactions Per Second(每秒传输的事物处理个数);QPS:每秒查询率。对于一个页面的一次访问,形成一个TPS;但一次页面请求,可能产生多次对服务器的请求,一次对服务器的请求是一次QPS 精准监控 – 对CPU利用率,load,内存,带宽,系统调用量,应用错误量,PV,UV和业务量进行监控,避免内存泄露和异常代码对系统产生影响,配置监控一定要精准。 无状态 – 服务器不能保存用户状态数据,如在集群环境下不能用static变量保存用户数据,不能长时间把用户文件存放在服务器本地。 容量规划 – 定期对容量进行评估。如大促前进行压测和容量预估,根据需要进行扩容。 功能开关 – 打开和关闭某些功能,比如消息量过大,系统处理不了,把开关打开后直接丢弃消息不处理。上线新功能增加开关,如果有问题关闭新功能。 设置超时 – 设置连接超时和读超时设置,不应该太大,如果是内部调用连接超时可以设置成1秒,读超时3秒,外部系统调用连接超时可以设置成3秒,读超时设置成20秒。 重试策略 – 当调用外部服务异常时可以设置重试策略,每次重试时间递增,但是需要设置最大重试次数和重试开关,避免对下游系统产生影响。 隔离 – 应用隔离,模块隔离,机房隔离【每个机房的服务都有自己的服务分组,本机房的服务应该只调用本机房服务,不进行跨机房调用;其中一个机房服务发生问题时可以通过DNS/负载均衡将请求全部切到另一个机房】,线程池隔离【把请求分类,然后交给不同的线程池处理,当一种业务的请求处理发生问题时,不会将故障扩散到其他线程池】,动静隔离【动态内容和静态资源分离,一般应该将静态资源放在CDN上】,热点隔离【秒杀、抢购等热点,需要独立系统或者服务器进行隔离,从而保证秒杀/抢购不会影响主流程】 异步调用 – 同步调用改成异步调用,解决远程调用故障或调用超时对系统的影响。 热点缓存 – 对热点数据进行缓存,降低RPC调用。 分级缓存 – 优先读本地缓存,其次读分布式缓存。通过推模式更新本地缓存。 服务降级 – 如果系统出现响应缓慢等状况,可以关闭部分功能,从而释放系统资源,保证核心服务的正常运行。 流量蓄洪 – 当流量陡增时,可以将请求进行蓄洪,如把请求保存在数据库中,再按照指定的QPS进行泄洪,有效的保护下游系统,也保证了服务的可用性。当调用对方系统,对方系统响应缓慢或无响应时,可采取自动蓄洪。 服务权重 – 在集群环境中,可自动识别高性能服务,拒绝调用性能低的服务。如在集群环境中,对调用超时的服务器进行权重降低,优先调用权重高的服务器。 依赖简化– 减少系统之间的依赖,比如使用消息驱动,A和B系统通过消息服务器传递数据,当A不可用时,短时间内不影响B系统提供服务。 灰度和回滚 – 发布新功能只让部分服务器生效,且观察几天逐渐切流,如果出现问题只影响部分客户。出现问题快速回滚,或者直接下线灰度的机器。 减少远程调用 熔断机制 – 增加熔断机制,当监控出线上数据出现大幅跌涨时,及时中断,避免对业务产生更大影响(可使用开源框架:hystrix(中文名“豪猪”))。 运行时加载模块 – 我们会把经常变的业务代码变成一个个业务模块,使用Java的ClassLoader在运行时动态加载和卸载模块,当某个模块有问题时候,可以快速修复。 自动备份 – 程序,系统配置和数据定期进行备份。可使用linux命令和shell脚本定时执行备份策略,自动进行本地或异地。出现问题时能快速重新部署。 线上压测 – 系统的对外服务需要进行压测,知道该服务能承受的QPS 和 TPS,从而做出相对准确的限流。
如何在基于Java的Web项目中实现文件上传和下载
在Sevlet3以前,Servlet API中没有支持上传功能的API,推荐使用Apache的commons-fileupload。
Sevlet3之后增加了Multipart支持可以直接实现文件的上传和下载
如何实现一个秒杀系统,保证只有几位用户能买到某件商品
1,页面开启倒计时,要保证不能把下单接口暴露过早暴露出来,防止机器刷下单接口 2,前端限流,比如nginx对下单接口限流,命中限流则返回302到秒杀页(nginx配置语句:limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;) 3,后端单独部署,独立域名和nginx,与线上正常运行的系统隔离开来,避免影响到线上环境 4,由于生成订单操作比较耗时,采用队列的方式来解耦下单成功和生成订单,针对进入后端的请求,采用redis自减,针对自减结果>0的请求则认为下单成功,触发一个生成订单的消息,然后立即返回给用户结果 5,用户方面,针对秒杀成功有两种处理方式 a,用户端收到秒杀成功的结果,则开启提示页面,并进入倒计时,倒计时时间为订单生成的预估时间 b,秒杀成功后,给当前用户在redis中生成一个订单生成状态的标识,用户端开启提示页面,loading,并轮询后端订单生成状态,生成成功之后让前端跳转到订单页面 6,订单服务订阅下单系统发送的消息,并开始生成订单,生成订单成功之后更新redis中用户秒杀订单的状态为已生成订单 系统应该有页面和接口,页面用于展示用户界面,接口用于获取数据 界面:秒杀页面,秒杀成功页面,秒杀失败页面,命中限流页面(查看订单页面不算秒杀系统的功能) 接口:秒杀下单接口,秒杀成功获取订单生成状态接口
如何实现负载均衡,有哪些算法可以实现
1、轮询法 将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。 2、随机法 通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多, 其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。 3、源地址哈希法 源地址哈希的思想是根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。 4、加权轮询法 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。 5、加权随机法 与加权轮询法一样,加权随机法也根据后端机器的配置,系统的负载分配不同的权重。不同的是,它是按照权重随机请求后端服务器,而非顺序。 6、最小连接数法 最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
如何设计建立和保持 100w 的长连接
可以使用Netty NIO框架,但需要扩大JVM内存,估计20G以上,JVM内存高了full GC时间会长可能会达到几秒,需要优化GC算法,
可以使用代理服务器来分散连接
负载均衡 + 反向代理。
如何避免浏览器缓存
1、HTML Meta标签控制缓存 可以在HTML页面的<head>节点中加入<meta>标签: <META HTTP-EQUIV="Pragma" CONTENT="no-cache"> <META HTTP-EQUIV="Expires" CONTENT="0"> 使用上很简单,但只有部分浏览器可以支持 2、HTTP头信息控制缓存 1) 在java代码中进行控制: response.setHeader( "Pragma", "no-cache" ); response.setDateHeader("Expires", 0); response.addHeader( "Cache-Control", "no-cache" );//浏览器和缓存服务器都不应该缓存页面信息 【 下面的代码设置强缓存: java.util.Date date = new java.util.Date(); response.setDateHeader("Expires",date.getTime()+20000); //Expires:过时期限值 response.setHeader("Cache-Control", "public"); //Cache-Control来控制页面的缓存与否,public:浏览器和缓存服务器都可以缓存页面信息; response.setHeader("Pragma", "Pragma"); //Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存 】 2) 通过配置web服务器的方式,让web服务器在响应资源的时候统一添加 Expires 和Cache-Control Header tomcat提供了一个ExpiresFilter专门用来配置强缓存。 nginx 配置: location ~ .*\.(js|css)$ { #如果频繁更新,则可以设置得小一点。 expires 1d; add_header Cache-Control max-age=86400; etag on; }
大型网站在架构上应当考虑哪些问题
1、海量数据的处理 2、数据并发的处理 3、文件存贮的问题 4、数据关系的处理 5、数据索引的问题 6、分布式处理 7、Ajax的利弊分析 8、数据安全性的分析 9、数据同步和集群的处理的问题 10、数据共享的渠道以及OPENAPI趋势
描述下常用的重构技巧
No.1:重复代码的提炼 No.2:冗长方法的分割 No.3:嵌套条件分支的优化(1) No.4:嵌套条件分支的优化(2) No.5:去掉一次性的临时变量 No.6:消除过长参数列表 No.7:提取类或继承体系中的常量 No.8:让类提供应该提供的方法 No.9:拆分冗长的类 No.10:提取继承体系中重复的属性与方法到父类
每个程序员要注意的 9 种反模式
1 过早优化 2 单车车库(避免花费太多时间在琐碎的事情上,比如花了好几个小时或者几天来讨论app的颜色问题。) 3 分析瘫痪(对问题的过度分析,阻碍了行动和进展。宁愿迭代,也不要过度分析和猜测。) 4 上帝类(上帝类是控制很多其它类,以及有很多依赖类,也就有更大的责任。) 5 新增类恐惧症 6 内部平台效应 7 魔法数和字符串(避免在代码中出现未注释、未命名的数字和字符串字面量。) 8 数字管理 9 无用的(幽灵)类(无用类本身没有真正的责任,经常用来指示调用另一个类的方法或增加一层不必要的抽象层。)
你们线上应用的 JVM 参数有哪些
-Xmx4g 设置JVM最大可用内存为4g。 -Xms4g 设置JVM初始内存为4g。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。 -XX:NewRatio=2 设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为2,则年轻代与年老代所占比值为1:2,年轻代占整个堆栈的1/3 -XX:SurvivorRatio=4 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:4,表示Eden:Survivor=4:2,一个Survivor区占整个年轻代的1/6 -XX:PermSize=256m 设置持久代大小为256m -XX:MaxPermSize=512m 设置持久代最大为512m -Xss256k 设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。 -XX:+DisableExplicitGC 关闭System.gc() -XX:+UseParNewGC 设置年轻代为并行收集,可与CMS收集同时使用,JDK5.0以上,JVM会根据系统配置自行设置,所以无需再设置此值 -XX:ParallelGCThreads=4 并行收集器的线程数,此值最好配置与处理器数目相等 同样适用于CMS -XX:+UseConcMarkSweepGC 使用CMS内存收集 -XX:+UseCMSCompactAtFullCollection 在FULL GC的时候,对年老代的压缩,CMS是不会移动内存的,因此非常容易产生碎片,导致内存不够用,因此内存的压缩这个时候就会被启用。增加这个参数是个好习惯。可能会影响性能,但是可以消除碎片 -XX:+CMSParallelRemarkEnabled 降低标记停顿 -XX:MaxTenuringThreshold=3 垃圾最大年龄,即对象在Survivor区存在的年龄为3(复制一次年龄+1),如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率该参数只有在串行GC时才有效. -XX:+CMSParallelRemarkEnabled 降低标记停顿(线上配置重复了) -XX:CMSInitiatingOccupancyFraction=70 使用cms作为垃圾回收,使用70%后开始CMS收集,为了保证不出现promotion failed(见下面介绍)错误,该值的设置需要满足以下公式CMSInitiatingOccupancyFraction计算公式 CMSInitiatingOccupancyFraction值与Xmn的关系公式: 上面介绍了promontion faild产生的原因是EDEN空间不足的情况下将EDEN与From survivor中的存活对象存入To survivor区时,To survivor区的空间不足,再次晋升到old gen区,而old gen区内存也不够的情况下产生了promontion faild从而导致full gc.那可以推断出:eden+from survivor < old gen区剩余内存时,不会出现promontion faild的情况,即: (Xmx-Xmn)*(1-CMSInitiatingOccupancyFraction/100)>=(Xmn-Xmn/(SurvivorRatior+2)) 进而推断出: CMSInitiatingOccupancyFraction <=((Xmx-Xmn)-(Xmn-Xmn/(SurvivorRatior+2)))/(Xmx-Xmn)*100 例如: 当xmx=128 xmn=36 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-36)-(36-36/(1+2)))/(128-36)*100 =73.913 当xmx=128 xmn=24 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((128.0-24)-(24-24/(1+2)))/(128-24)*100=84.615… 当xmx=3000 xmn=600 SurvivorRatior=1时 CMSInitiatingOccupancyFraction<=((3000.0-600)-(600-600/(1+2)))/(3000-600)*100=83.33 CMSInitiatingOccupancyFraction低于70% 需要调整xmn或SurvivorRatior值。 -XX:CMSFullGCsBeforeCompaction=0 多少次后进行内存压缩,由于并发收集器不对内存空间进行压缩,整理,所以运行一段时间以后会产生"碎片",使得运行效率降低.此值设置运行多少次GC以后对内存空间进行压缩,整理. -XX:+UseFastAccessorMethods 原始类型的快速优化 -XX:+UseBiasedLocking 锁机制的性能改善 -Dcom.sun.management.jmxremote 使用jvisualvm通过JMX的方式远程监控JVM的运行情况,还要求再配置ssl、port、authenticate等参数,单独配置这个可能没有,或者可以使用默认配置,需要确认??? -Djava.awt.headless=true 有时我们会在我们的J2EE工程中使用一些图表工具如:jfreechart,用于在web网页输出GIF/JPG等流,在winodws环境下,一般我们的app server在输出图形时不会碰到什么问题,
但是在linux/unix环境下经常会碰到一个exception导致你在winodws开发环境下图片显 示的好好可是在linux/unix下却显示不出来,因此加上这个参数以免避这样的情况出现. 调试参数: -verbose:gc 表示输出虚拟机中GC的详细情况 -XX:+PrintGC 加上参数可以在输出日志中可以查看垃圾回收前后堆的大小, 即打印gc日志 -XX:+PrintGCDetails 打印gc日志的更加详细的信息 -XX:+PrintGCDateStamps (-XX:+PrintGCDateStamps或者-XX:+PrintGCTimeStamps),输出GC发生时,gc发生的时间 -XX:+PrintAdaptiveSizePolicy 打印自适应收集的大小。默认关闭。 -XX:+PrintHeapAtGC 打印GC前后的详细堆栈信息 -XX:+PrintTenuringDistribution 查看每次minor GC后新的存活周期的阈值 -Xloggc:/***/gc.log 把相关日志信息记录到文件以便分析(gc.log) -XX:ErrorFile=/**/hs_err_pid.log 生成error 文件的路径(hs_err_pid.log) -XX:HeapDumpPath=/**/java.hprof 指定HeapDump的文件路径或目录(java.hprof) 配置地址: -Dconfig.home=/**/config config配置文件路径 -Dlogback.configurationFile=/**/logback.xml logback配置文件logback.xml文件位置
GC日志包括些什么内容?
主要内容包括: 1、时间戳、相对时间戳; 2、GC回收类型(yong/old)、垃圾回收区域(不同的垃圾回收器,对应的区域名词可能不同,包括:DefNew<串行-年轻代>、Tenured<老年代>、Perm<方法区>、PSYoungGen<ParNew-年轻代>、ParOldGen<ParNew-老年代>等); 3、年轻代(老年代)回收前大小、年轻代(老年代)回收后大小、年轻代(老年代)总大小、堆回收前大小、堆回收后大小、堆总大小; 4、GC的CPU耗时、系统唤醒GC耗时、总耗时
怎样看新生代GC日志?
新生代回收的一行日志: 2014-07-18T16:02:17.606+0800: 611.633: [GC 611.633: [DefNew: 843458K->2K(948864K), 0.0059180 secs] 2186589K->1343132K(3057292K), 0.0059490 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 含义大概如下: 2014-07-18T16:02:17.606+0800【当前时间戳】: 611.633【相对<JVM启动的>时间戳】: [GC【表示Young GC】 611.633: [DefNew【单线程Serial年轻代GC】: 843458K【年轻代垃圾回收前的大小】->2K【年轻代回收后的大小】(948864K【年轻代总大小】), 0.0059180 secs【本次回收用时】] 2186589K【整个堆回收前的大小】->1343132K【整个堆回收后的大小】(3057292K【堆总大小】), 0.0059490 secs【回收时间】] [Times: user=0.00【cpu耗时】 sys=0.00【系统唤醒GC耗时】, real=0.00 secs【实际耗时,等于前两个和】]
怎样看老年代GC日志?
和新生代类似,老年代回收的一行日志: 2014-07-18T16:19:16.794+0800: 1630.821: [GC 1630.821: [DefNew: 1005567K->111679K(1005568K), 0.9152360 secs]1631.736: [Tenured: 2573912K->1340650K(2574068K), 1.8511050 secs] 3122548K->1340650K(3579636K), [Perm : 17882K->17882K(21248K)], 2.7854350 secs] [Times: user=2.57 sys=0.22, real=2.79 secs] 含义如下: 2014-07-18T16:19:16.794+0800【时间戳】: 1630.821【相对<JVM启动的>时间戳】: [GC【年轻代GC】 1630.821: [DefNew【使用的垃圾回收器】: 1005567K【年轻代回收前大小】->111679K【年轻代回收后大小】(1005568K【年轻代总大小】), 0.9152360 secs【回收用时】]1631.736【此时相对<JVM启动的>时间戳】: [Tenured【老年代】: 2573912K【老年代回收前大小】->1340650K【老年代回收后大小】(2574068K【【老年代总大小】】), 1.8511050 secs] 3122548K【整个堆回收前大小】->1340650K【整个堆回收后大小】(3579636K【堆总大小】), [Perm : 17882K【方法区回收前大小】->17882K【方法区回收后大小】(21248K【方法区总大小】)], 2.7854350 secs【回收用时】] [Times: user=2.57【cpu耗时】 sys=0.22【系统唤醒GC耗时】, real=2.79 secs【总耗时】]
完整GC日志举例
[GC [PSYoungGen: 4423K->320K(9216K)] 4423K->320K(58880K), 0.0011900 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [Full GC (System) [PSYoungGen: 320K->0K(9216K)] [ParOldGen: 0K->222K(49664K)] 320K->222K(58880K) [PSPermGen: 2458K->2456K(21248K)], 0.0073610 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap PSYoungGen total 9216K, used 491K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000) eden space 8192K, 6% used [0x00000000ff600000,0x00000000ff67af50,0x00000000ffe00000) from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000) to space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000) ParOldGen total 49664K, used 222K [0x00000000c5800000, 0x00000000c8880000, 0x00000000ff600000) object space 49664K, 0% used [0x00000000c5800000,0x00000000c58378a0,0x00000000c8880000) PSPermGen total 21248K, used 2466K [0x00000000c0600000, 0x00000000c1ac0000, 0x00000000c5800000) object space 21248K, 11% used [0x00000000c0600000,0x00000000c0868b48,0x00000000c1ac0000) gc日志中的最后(Heap往后的部分)貌似是系统运行完成前的快照
解释什么是 MESI 协议(缓存一致性)
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。 MESI协议中的状态 CPU中每个缓存行(caceh line)使用4种状态进行标记(使用额外的两位(bit)表示): M: 被修改(Modified) 该缓存行只被缓存在该CPU的缓存中,并且是被修改过的(dirty),即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回(write back)主存。当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。 E: 独享的(Exclusive) 该缓存行只被缓存在该CPU的缓存中,它是未被修改过的(clean),与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。 S: 共享的(Shared) 该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致(clean),当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。 I: 无效的(Invalid) 该缓存是无效的(可能有其它CPU修改了该缓存行)。
谈谈 reactor 模型
Reactor模式,这个模式是出现在NIO中。
反应器设计模式(Reactor pattern)是一种为处理并发服务请求,并将请求提交到一个或者多个服务处理程序的事件设计模式。当客户端请求抵达后,服务处理程序使用多路分配策略,由一个非阻塞的线程来接收所有的请求,然后派发这些请求至相关的工作线程进行处理。
Reactor模式主要包含下面几部分内容。
初始事件分发器(Initialization Dispatcher):用于管理Event Handler,定义注册、移除EventHandler等。
同步(多路)事件分离器(Synchronous Event Demultiplexer):无限循环等待新事件的到来,一旦发现有新的事件到来,就会通知初始事件分发器去调取特定的事件处理器。
系统处理程序(Handles):操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。
事件处理器(Event Handler): 定义事件处理方法,以供Initialization Dispatcher回调使用。
对于Reactor模式,可以将其看做由两部分组成,一部分是由Boss组成,另一部分是由worker组成。Boss就像老板一样,主要是拉活儿、谈项目,一旦Boss接到活儿了,就下发给下面的work去处理。也可以看做是项目经理和程序员之间的关系。
reactor设计模式,是一种基于事件驱动的设计模式。Reactor框架是ACE各个框架中最基础的一个框架,其他框架都或多或少地用到了Reactor框架。
虚拟内存是什么
虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
与没有使用虚拟内存技术的系统相比,使用这种技术的系统使得大型程序的编写变得更容易,对真正的物理内存(例如RAM)的使用也更有效率。
注意:虚拟内存不只是“用磁盘空间来扩展物理内存”的意思——这只是扩充内存级别以使其包含硬盘驱动器而已。把内存扩展到磁盘只是使用虚拟内存技术的一个结果,它的作用也可以通过覆盖或者把处于不活动状态的程序以及它们的数据全部交换到磁盘上等方式来实现。
阐述下 SOLID 原则
SOLID(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)是由罗伯特·C·马丁在21世纪早期[1] 引入的记忆术首字母缩略字
请简要讲一下你对测试驱动开发(TDD)的认识
测试驱动开发(Test-driven development,缩写为TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发始于20世纪90年代。
测试驱动开发的目的是取得快速反馈并使用“illustrate the main line”方法来构建程序。
测试驱动开发是戴两顶帽子思考的开发方式:先戴上实现功能的帽子,在测试的辅助下,快速实现其功能;再戴上重构的帽子,在测试的保护下,通过去除冗余的代码,提高代码质量。测试驱动着整个开发过程:首先,驱动代码的设计和功能的实现;其后,驱动代码的再设计和重构。
正面评价
可以有效的避免过度设计带来的浪费。但是也有人强调在开发前需要有完整的设计再实施可以有效的避免重构带来的浪费。
可以让开发者在开发中拥有更全面的视角。
负面评价
开发者可能只完成满足了测试的代码,而忽略了对实际需求的实现。有实践者认为用结对编程的方式可以有效的避免这个问题。
会放慢开发实际代码的速度,特别对于要求开发速度的原型开发造成不利。这里需要考虑开发速度需要包含功能和品质两个方面,单纯的代码速度可能不能完全代表开发速度。
什么是 zab 协议
ZAB ( ZooKeeper Atomic Broadcast , ZooKeeper 原子消息广播协议)是zookeeper数据一致性的核心算法。 ZAB 协议并不像 Paxos 算法(一种通用的分布式一致性算法),它是一种特别为 ZooKeeper 设计的崩溃可恢复的原子消息广播算法。 ZAB协议主要实现了: 1.使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用 ZAB 的原子广播协议,将服务器数据的状态变更以事务 Proposal 的形式广播到所有的副本进程上去。 2.保证一个全局的变更序列被顺序应用。比如P1的事务t1可能是创建节点“/a”,t2可能是创建节点“/a/aa”,只有先创建了父节点“/a”,才能创建子节点“/a/aa”。 3.当前主进程出现异常情况的时候,依旧能够正常工作。 ZAB 协议的核心:定义了事务请求的处理方式。 所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为 Leader服务器,而余下的其他服务器则成为 Follower 服务器。 Leader 服务器负责将一个客户端事务请求转换成一个事务proposal(提议),并将该 Proposal分发给集群中所有的Follower服务器。
之后 Leader 服务器需要等待所有Follower 服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么 Leader 就会再次向所有的 Follower服务器分发Commit消息,要求其将前一个proposal进行提交。
ZAB 协议的两种基本的模式
分别是崩溃恢复和消息广播。
当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时, ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的Leader 服务器同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,
ZAB 协议就会退出恢复模式。
当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播 ,
那么新加人的服务器就会自觉地进人数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。
zookeeper 如何选举的?
在集群初始化阶段,当有一台服务器ZK1启动时,其单独无法进行和完成Leader选举,当第二台服务器ZK2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进入Leader选举过程。选举过程开始,过程如下: (1) 每个Server(服务器状态为LOOKING)发出一个投票。ZK1和ZK2都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时ZK1的投票为(1, 0),ZK2的投票为(2, 0),然后各自将这个投票发给集群中其他机器。 (2) 接受来自各个服务器的投票。集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器。 (3) 处理投票。针对每一个投票,服务器都需要将别人的投票和自己的投票进行比较,规则如下 · 优先检查ZXID。ZXID比较大的服务器优先作为Leader。 · 如果ZXID相同,那么就比较myid。myid较大的服务器作为Leader服务器。 对于ZK1而言,它的投票是(1, 0),接收ZK2的投票为(2, 0),首先会比较两者的ZXID,均为0,再比较myid,此时ZK2的myid最大,于是ZK2胜。ZK1更新自己的投票为(2, 0),并将投票重新发送给ZK2。 (4) 统计投票。每次投票后,服务器都会统计投票信息,判断是否已经有过半机器接受到相同的投票信息,如果有,则就是Leader。 (5) 改变服务器状态。一旦确定了Leader,每个服务器就会更新自己的状态,如果是Follower,那么就变更为FOLLOWING,如果是Leader,就变更为LEADING。 当新的Zookeeper节点ZK3启动时,发现已经有Leader了,不再选举,直接将直接的状态从LOOKING改为FOLLOWING。
为什么说 zookeeper 集群 最好是奇数个节点?
Zookeeper 有三种运行模式:单机模式、伪集群模式和集群模式。集群模式有“ 过半存活即可用 ”的特性: 2节点zookeeper,一个主节点挂了,另外一个备节点因为没有过半,无法对外提供集群服务,容错数为0 5节点zookeeper,两个主节点挂了,另外三个备节点过半,对外提供集群服务,容错数为2 6节点zookeeper,两个主节点挂了,另外四个备节点过半,对外提供集群服务,容错数为2;如果挂了三个节点,则无法对外提供服务 总结: 1.成功选举Leader必须要备节点过半,2n和2n-1(n>1)的容错数是一样的都是 n-1 。 2.集群服务偶数节点也是可以的,偶数容错数和奇数一样,所以没必要浪费一个节点资源。 所以,最好是奇数个,并不是不能是偶数个,因为偶数个(2n个节点)的容错率和奇数个(2n-1)的容错率一样,没必要浪费一个节点。
什么是领域模型(domain model)?贫血模型(anaemic domain model) 和充血模型(rich domain model)有什么区别
业务对象模型(也叫领域模型 domain model)是描述业务用例实现的对象模型。
贫血模型是指使用的领域对象中只有setter和getter方法(POJO),所有的业务逻辑都不包含在领域对象中而是放在业务逻辑层。
充血模型将大多数业务逻辑和持久化放在领域对象中,业务逻辑(业务门面)只是完成对业务逻辑的封装、事务和权限等的处理。
什么是领域驱动开发(Domain Driven Development)
将问题抽象为一个领域解决方案。并针对此领域(即抽象)进行开发的方式。 领域模型的核心是抽象和分治(less is more)。 问题分层、问题分块、关注要素,忽略细节 ;关注抽象,忽略具体……
HTTPS和HTTP的区别
在应用层(http)和传输层(tcp)间加了一层会话层(SSL) 在URL前加https://前缀表明是用SSL加密的。 你的电脑与服务器之间收发的信息传输将更加安全。 Web服务器启用SSL需要获得一个服务器证书并将该证书与要使用SSL的服务器绑定。 http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。http的连接很简单,是无状态的,... HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议 要比http协议安全
传输层常见编程协议有哪些?并说出各自的特点
TCP协议:面向连接的可靠传输协议。利用TCP进行通信时,首先要通过三步握手,以建立通信双方的连接。TCP提供了数据的确认和数据重传的机制,保证发送的数据一定能到达通信的对方。
UDP协议:是无连接的,不可靠的传输协议。采用UDP进行通信时不用建立连接,可以直接向一个IP地址发送数据,但是不能保证对方是否能收到
可以使用同步发送消息