JVM
内存溢出是怎么回事?请举例。**
答:
- 堆栈溢出 在JVM内存结构中,有一块是堆内存,主要用于存放对象。这块堆内存是有容量大小的(-Xmx,-Xms),在程序运行过程中,对象实际要使用大小超过堆内存总大小,JVM的垃圾会后策略又不能回收足够的空间用于存放这些对象,则可能触发内存溢出。溢出错误可能如下: java.lang.OutOfMemoryError: ......java heap space..... java.lang.OutOfMemoryError:GC over head limit exceeded
== 和equals 的区别?**
答:
- 作用于java自带的8中基本数据类型时:
(short,boolean,int,byte,char,long,double,float) ==和equals都表示:比对变量的值是否相等。
- 作用于String,Integer,Date等Java中覆盖了equals方法的类的对象时:
== 表示比对的是两个对象的地址是否相等; equals 已经不再是比对内存地址,例如:String比对两个字符串对象的字面量是否相等;
- 作用于其它对象时
== 表示比对的是对象的地址是否相等; equals Object中equals方法的初始行为是比对两个对象的内存地址。所有对象都继承于Object这个基类,因此,子类可能会覆盖父类的equals方法,对于覆盖equeals方法的类,会由实现方法决定比对行为。
hashCode方法的作用,为什么重写equals方法,建议一起重写hashcode方法?**
答:
- 通过一系列的算法,将多个任意长度的输入,映射为固定长度的值,并将这些值组成一张表,称它为Hash表,hashcode代表每个输入在hash表中对应的位置。
- hashCode怎么生成的呢?例如:hash表中有hashcode为:1,2,3,4,5,6,7,8这样8个位置,当有一个对象地址为:17,那么可以用:17%8 = 1, 将17存放到hashcode为1 的位置。
- hashcode的作用:实现快速查找。先将对象的地址进行hash,缩小对象比对范围,然后再进行对象比对。例如:有一个能放1000个数这样大的内存,如果要查找某个值,比较笨的方法,遍历,最差比对1000次。如果使用hashcode,直接通过value%8 得到位置,然后在对位置上所有的对象进行比对(因为一个位置可能存了多个对象),这种方法就减少了很多比对次数。要和同一位置上的对象比对,那么就要用到equals方法。
- 简单来说就是:先hash,再比对。
- 如果两个对象equals相等,那么这两个对象的HashCode也一定相等。
- 如果两个对象的hashcode一样,但是不一定是同一对象。
- 为什么重写equals方法,建议重写hashcode方法呢。假如:重写了equals方法,两个对象equals结果相等,但hashcode不相等,这就破坏了Hashcode的规定。
NIO是什么?适用于何种场景
简述JVM内存结构
答:
JVM在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。 * 程序计数器 * 线程私有,是当前线程所执行的字节码的行号指示器 * 通过改变这个计数器的值来选择下一条需要执行的字节码指令:分支,循环,跳转,异常处理,线程恢复等 * 唯一一个没有规定任何OutofMemoryError情况的区域。
- Java虚拟机栈
-
线程私有,随线程退出而消亡,是java方法执行的内存模型,用于存放:局部变量,操作数,动态链接,方法出口等信息,局部变量表存放编译期可知的各种基本数据类型、对象引用、returnAdress类型。
-
本地方法栈
-
虚拟机使用到的native方法服务,随线程而生,随线程而灭
-
java堆
- 主要存放对象,所有线程共享,分为新生代,老年代,永久代
- 新生代:Eden空间,From Survivor, To Survivor空间
-
线程共享的Java堆上可以划分出线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)
-
方法区
- 各个线程共享
- 存放类信息,静态变量,常量,即时编译器编译后的代码数据等
- 习惯于把方法区称为永久代
-
针对这个区的gc,主要是对常量池和类的卸载
-
运行时常量池
- 方法区的一部分
-
存放编译期生成的字面量和符号引用
-
直接内存
- 不是虚拟机运行时数据区的一部分
- 不是Java虚拟机中规范的内存区的一部分
- 不受到Java堆大小的限制
- 1.4中引入通道与缓冲区的I/O方式,使用Native函数库直接分配堆外内存,然后通过存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,显著提高性能:避免了在java对和Native堆中来回复制数据
JVM堆的基本结构
JVM的内存分配
如何用Java分配一段连续的1G的内存空间?需要注意些什么
如何查看JVM的内存使用情况
为什么需要GC,都有哪些回收算法
答:
GC是垃圾回收的意思,在JVM中,几乎所有的对象和变量信息都保存在内存的堆和栈中。在java堆中,创建对象时,为对象分配内存,在对象生命周期结束时,需要对对象所占用的的内存进行回收,以供为后续新生成对象分配空间复用。如果不能够对这些内存进行回收,则会用完所有内存,导致系统内存溢出,甚至系统崩溃。因此,需要有GC回收内存。 GC算法思想有如下几种: * 标记-整理算法 * 标记-清除算法 * 分代收集算法
CMS垃圾回收的基本流程
Jvm有自己的内存回收机制,但为什么还存在内存泄露的问题呢
答:
java内存泄露的根本原因:内存对象明明已经不需要的时候,还仍然保留着这块内存和它的访问方式。也称作:无意识的对象保持。 java的GC机制: * 引用计数算法:计数器 * 可达性分析 通过一系列的称为“GC roots”的对象最为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC roots没有任何引用链相连时,则证明此对象是不可引用的。
GC roots:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
java中对内存对象的访问,使用的是引用的方式。在 Java 代码中我们维护一个内存对象的引用变量,通过这个引用变量的值,我们可以访问到对应的内存地址中的内存对象空间。在 Java 程序中,这个引用变量本身既可以存放堆内存中,又可以放在代码栈的内存中(与基本数据类型相同)。 GC 线程会从代码栈中的引用变量开始跟踪,从而判定哪些内存是正在使用的。如果 GC 线程通过这种方式,无法跟踪到某一块堆内存,那么 GC 就认为这块内存将不再使用了(因为代码中已经无法访问这块内存了)。
通过这种有向图的内存管理方式,当一个内存对象失去了所有的引用之后,GC 就可以将其回收。反过来说,如果这个对象还存在引用,那么它将不会被 GC 回收,哪怕是 Java 虚拟机抛出 OutOfMemoryError
Java程序是否会内存溢出,内存泄露情况发生?举几个例子
常用的GC策略,什么时候会触发YGC,什么时候触发FGC
System.gc()和Runtime.gc()会做什么事情
finalize()方法什么时候被调用?析构函数(finalization)的目的是什么
如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用它的内存
创建一个类的实例都有哪些办法?(OK)
答:
- 通过new关键字
- 调用了构造函数
- 通过反射机制
- 通过克隆
- 不调用构造函数,需要先实现Cloneable接口
- 通过反序列化
- 不调用构造函数
* new 创建对象
Person person = new Person();
* 反射机制1
Person emp2 = (Person) Class.forName("org.programming.mitra.exercises.Person").newInstance();
或者
Persone emp2 = Person.class.newInstance();
* 反射机制2
Constructor<Person > constructor = Person .class.getConstructor();
Person emp3 = constructor.newInstance();
* clone
class Person implements Cloneable{
...}
Person p1 = new Person();
Person p2 = null;
try{
p2 = (Person)p1.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();}
* 反序列化
// By using Deserialization
// Serialization
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.obj"));
out.writeObject(emp4);
out.close();
//Deserialization
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.obj"));
Employee emp5 = (Employee) in.readObject();
in.close();
emp5.setName("Akash");
System.out.println(emp5 + ", hashcode : " + emp5.hashCode());
判断对象是否存活的方法?
Java 类加载过程?
- 加载
根据类的全限定名,获取二进制字节流
将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生产一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证
目的:确保class文件字节流中包含的信息符合当前虚拟机的要求,不会危害到虚拟机自身的安全
1. 验证class文件格式,字节流是否符合class文件格式要求,是否能被虚拟机处理;例如:是否以魔数开头(0xCAFEBABE)开头,主次版本号是否在当前虚拟机支持范围内,常量池中是否有不被支持的常量;
2. 元数据验证:保证描述的信息符合java语言规范;例如:是否有父类,是否继承了不允许被继承的类,是否实现了接口中的方法,是否与父类产生矛盾;
3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的,符合语义的;例如:保证跳转指令不会跳转到方法体以外的字节码上;保证方法体上的类型转换是有效的;
4. 符号引用验证,确保解析动作能正常执行:对类自身以外的信息进行匹配性校验(常量池中的各种符号引用);例如:符号引用中通过字符串描述的全限定名是否能找到对应的类;
备注:验证非虚拟机必须的阶段,可以通过-Xverify:none 跳过大部分验证
- 准备
为变量分配内存,并为类变量设置初始值的阶段
在方法区中进行分配
进行内存分配的只是类变量(被static修饰的变量),不包括实例变量
- 解析
虚拟机将常量池内的符号引用替换为直接引用的过程
1. 类或者接口的解析
2. 字段解析
3. 类方法解析
4. 接口方法解析
- 初始化
真正开始执行类中定义的Java程序代码
初始化阶段是执行类构造器<clinit>()方法的过程
- 使用
- 卸载
描述一下JVM加载Class文件的原理机制
什么是类加载器,类加载器有哪些方法
- 类加载器
用于实现类的加载动作
确定类在java虚拟机中的唯一性
每个类加载器都有一个独立的类名称空间
比较两个类是否相同:只有在两个类是由同一个类加载器加载的前提下才有意义
- 从java虚拟机的角度看,只存在两种类加载器
1. 启动类加载器(Bootstrap ClassLoader),用C++实现,是虚拟机的一部分;
2. 所有其他的类加载器,由java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader
- 绝大部分Java程序都会使用到以下3种系统提供的类加载器
1. 启动类加载器;<JAVA_HOME>/lib目录中,或者-Xbootclasspath参数所指定的路径中的类,并且是虚拟机识别的类(按照名字);
2. 扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,可以直接使用扩展类加载器;
3. 应用程序类加载器:由sun.misc.Launcher$AppClassLoader实现,加载用户类路径上的类
类加载器双亲委派模型机制
- 类加载器之间的层次关系称为类加载器的双亲委派模型
:
启动类加载器
^
|
扩展类加载器
^
|
应用程序类加载器
^ ^
| |
自定义类加载器 自定义类加载器
- 如果一个类加载器收到了类加载的请求,不会自己去加载这个类,而是把这个请求委派给父类加载器去完成;
- 每个层次的类加载器都是如上工作,直到请求传送到顶层的启动类加载器中
- 当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
*优点:
通用的类(例如java.lang.Object),可以由最顶端的启动类加载器进行加载,保证类体系中最基础的行为。
如果自己编写Object的加载方法,加载方法不一样,将会变得一片混乱。
*双亲委派模型对于Java程序的稳定运行有很重要的作用。 * 代码实现全部在java.lang.ClassLoader的loadClass()中
- JNDI破坏了双亲委派模型
JVM有哪些常用启动参数可以调整,描述几个?
- -Xmx3550m
设置JVM最大可用内存为3550M
- -Xms3550m
设置JVM初始内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存
- -Xss128k
设置每个线程的堆栈大小。(JDK5.0以后每个线程堆栈大小为1M)
- -Xmn1024m
设置年轻代大小为1024m。等效于同时配置NewSize和MaxNewSize两个。
- -XX:NewSize=1024m
设置年轻代初始值为1024M。
- -XX:MaxNewSize=1024m
设置年轻代最大值为1024M。
-
JVM8.0已经没有持久代(XX:PermSize和MaxPermSize),若设置则会警告 Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10m; support was removed in 8.0
-
串行收集器
-XX:+UseSerialGC:设置串行收集器。
并行收集器(吞吐量优先)
-XX:+UseParallelGC:设置年轻代为并行收集器。(此时年老代仍然为串行)
-XX:+UseParallelOldGC:配置年老代为并行收集。
-XX:ParallelGCThreads=20:配置并行收集器的线程数。
-XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间(单位毫秒)。如果无法满足此时间,JVM会自动调整年轻代大小,以满足此时间。
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动调整年轻代Eden区大小和Survivor区大小的比例,以达成目标系统规定的最低响应时间或者收集频率等指标。此参数建议在使用并行收集器时,一直打开。
- 并发收集器(响应时间优先)
-XX:+UseConcMarkSweepGC:即CMS收集,设置年老代为并发收集。
-XX:+UseParNewGC:设置年轻代为并发收集。JDK5.0以上JVM会自行设置,无需再设。
-XX:CMSFullGCsBeforeCompaction=0:每次Full GC后立刻开始压缩和整理内存。
-XX:+UseCMSCompactAtFullCollection:打开内存空间的压缩和整理,在Full GC后执行。
-XX:+CMSIncrementalMode:设置为增量收集模式。一般适用于单CPU情况。
-XX:CMSInitiatingOccupancyFraction=70:表示年老代内存空间使用到70%时就开始执行CMS收集,以确保年老代有足够的空间接纳来自年轻代的对象,避免Full GC的发生。
* 其它垃圾回收参数
-XX:+ScavengeBeforeFullGC:年轻代GC优于Full GC执行。
-XX:-DisableExplicitGC:不响应 System.gc() 代码。
-XX:+UseThreadPriorities:启用本地线程优先级API。即使 java.lang.Thread.setPriority() 生效,不启用则无效。
-XX:SoftRefLRUPolicyMSPerMB=0:软引用对象在最后一次被访问后能存活0毫秒(JVM默认为1000毫秒)。
-XX:TargetSurvivorRatio=90:允许90%的Survivor区被占用(JVM默认为50%)。提高对于Survivor区的使用率。
- 辅助参数
-XX:-CITime:打印消耗在JIT编译的时间。
-XX:ErrorFile=./hs_err_pid.log:保存错误日志或数据到指定文件中。
-XX:HeapDumpPath=./java_pid.hprof:指定Dump堆内存时的路径。
-XX:-HeapDumpOnOutOfMemoryError:当首次遭遇内存溢出时Dump出此时的堆内存。
-XX:OnError=";":出现致命ERROR后运行自定义命令。
-XX:OnOutOfMemoryError=";":当首次遭遇内存溢出时执行自定义命令。
-XX:-PrintClassHistogram:按下 Ctrl+Break 后打印堆内存中类实例的柱状信息,同JDK的 jmap -histo 命令。
-XX:-PrintConcurrentLocks:按下 Ctrl+Break 后打印线程栈中并发锁的相关信息,同JDK的 jstack -l 命令。
-XX:-PrintCompilation:当一个方法被编译时打印相关信息。
-XX:-PrintGC:每次GC时打印相关信息。
-XX:-PrintGCDetails:每次GC时打印详细信息。
-XX:-PrintGCTimeStamps:打印每次GC的时间戳。
-XX:-TraceClassLoading:跟踪类的加载信息。
-XX:-TraceClassLoadingPreorder:跟踪被引用到的所有类的加载信息。
-XX:-TraceClassResolution:跟踪常量池。
-XX:-TraceClassUnloading:跟踪类的卸载信息。
你常用的JVM配置和调优参数都有哪些?分别什么作用?
- 高优先级:-XX:NewSize/-XX:MaxNewSize
- 中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
- 低优先级:-XX:NewRatio
-
推荐使用-Xmn参数,原因是这个参数简洁,相当于一次设定 NewSize/MaxNewSIze,而且两者相等,适用于生产环境。-Xmn 配合 -Xms/-Xmx,即可将堆内存布局完成
-
大型网站服务器案例:承受海量访问的动态Web应用,服务器配置:8 CPU, 8G MEM, JDK 1.6.X 参数方案:
-server -Xmx3550m -Xms3550m -Xmn1256m -Xss128k -XX:SurvivorRatio=6 -XX:MaxPermSize=256m -XX:ParallelGCThreads=8 -XX:MaxTenuringThreshold=0 -XX:+UseConcMarkSweepGC
-Xmx 与 -Xms 相同以避免JVM反复重新申请内存。-Xmx 的大小约等于系统内存大小的一半,即充分利用系统资源,又给予系统安全运行的空间。
-Xmn1256m 设置年轻代大小为1256MB。此值对系统性能影响较大,Sun官方推荐配置年轻代大小为整个堆的3/8。
-Xss128k 设置较小的线程栈以支持创建更多的线程,支持海量访问,并提升系统性能。
-XX:SurvivorRatio=6 设置年轻代中Eden区与Survivor区的比值。系统默认是8,根据经验设置为6,则2个Survivor区与1个Eden区的比值为2:6,一个Survivor区占整个 年轻代的1/8。
-XX:ParallelGCThreads=8 配置并行收集器的线程数,即同时8个线程一起进行垃圾回收。此值一般配置为与CPU数目相等。
-XX:MaxTenuringThreshold=0 设置垃圾最大年龄(在年轻代的存活次数)。如果设置为0的话,则年轻代对象不经过Survivor区直接进入年老代。对于年老代比较多的应 用,可以提高效率;如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率 。根据被海量访问的动态Web应用之特点,其内存要么被缓存起来以减少直接访问DB,要么被快速回收以支持高并发海量请求,因此其内存对象在年轻代存活多次意义不 大,可以直接进入年老代,根据实际应用效果,在这里设置此值为0。
-XX:+UseConcMarkSweepGC 设置年老代为并发收集。CMS(ConcMarkSweepGC)收集的目标是尽量减少应用的暂停时间,减少Full GC发生的几率,利用和应用程序线程并 发的垃圾回收线程来标记清除年老代内存,适用于应用中存在比较多的长生命周期对象的情况。
- 内部集成构建服务器案例, 高性能数据处理的工具应用, 服务器配置:1 CPU, 4G MEM, JDK 1.6.X, 参数方案:
-server -XX:PermSize=196m -XX:MaxPermSize=196m -Xmn320m -Xms768m -Xmx1024m
调优说明:
-XX:PermSize=196m -XX:MaxPermSize=196m 根据集成构建的特点,大规模的系统编译可能需要加载大量的Java类到内存中,所以预先分配好大量的持久代内存是高效和 必要的。
-Xmn320m 遵循年轻代大小为整个堆的3/8原则。
-Xms768m -Xmx1024m 根据系统大致能够承受的堆内存大小设置即可。
深拷贝和浅拷贝
- 浅拷贝只创建新的对象,对于对象里的属性对象,使用的是原拷贝对象的对象
- 深拷贝,deep clone,官方的定义为对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝
如何创建线程
答:
在java中通过两种方式创建线程:
- 继承Thread类
- 重写run方法
- 通过start启动线程
- 本质上Thread也是实现Runnable接口
- 实现Runnable接口
- 重写run方法
- 将实现类作为参数传递给Thread
- 通过start启动线程
两种方法的不同:
- 在需要进行继承的情况下,只能使用Runnable
线程和进程的差别是什么?
- 进程是资源分配的最小单位,线程是CPU调度的最小单位
什么是线程安全,如何保证线程安全
答:
-
在多线程环境中,某个线程开始访问到访问结束的整个过程,没有任何一个访问对象被其它线程修改,那么我们就说是线程安全的,线程安全也就是能永远保证程序的正确性。
-
多线程环境下,线程运行结果和单线程运行结果一样,而且其它的变量的值和逾期的也是一样,可以认为是线程安全的。
-
一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不 用考虑同步的问题。
引起线程安全问题的根本原因是: * 多线程环境,即同时存在多个操作者,单线程环境不存在线程安全问题;
-
多个操作者(线程)必须操作同一个对象,才可能引起线程安全;
-
多个操作者(线程)必须对同一个对象存在修改操作;
综上可知,线程安全问题产生的根本原因:
共享数据存在被并发修改的可能
线程安全已发的问题:
-
死锁
-
脏数据
保证线程安全的方法:
- 消除共享数据
- 使用线程同步不机制:锁和同步
只有存在共享数据时才需要考虑线程安全问题,从JVM管理的内存空间来看,在如下区域需要考虑线程安全
-
java堆
-
方法区
如何避免死锁
Volatile关键字的作用?
答:
- 保持内存可见性
- 禁止指令重排序
并发编程中三个问题:
- 原子性
一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
- 可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其它线程能够立即看得到修改的值。
- 有序性
即程序执行的顺序按照代码的先后顺序执行 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行 volatile也无法保证对变量的任何操作都是原子性的 * 观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能: * 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; * 它会强制将对缓存的修改操作立即写入主存; * 如果是写操作,它会导致其他CPU中对应的缓存行无效;
使用volatile关键字的场景:
- 对变量的写操作不依赖于当前值;
- 该变量没有包含在具有其他变量的不变式中;
备注:
java通过集中原子操作完成工作内存和主内存的交互
- lock:作用于主内存,把变量标识为线程独占状态。
- unlock:作用于主内存,解除独占状态。
- read:作用主内存,把一个变量的值从主内存传输到线程的工作内存。
- load:作用于工作内存,把read操作传过来的变量值放入工作内存的变量副本中。
- use:作用工作内存,把工作内存当中的一个变量值传给执行引擎。
- assign:作用工作内存,把一个从执行引擎接收到的值赋值给工作内存的变量。
- store:作用于工作内存的变量,把工作内存的一个变量的值传送到主内存中。
- write:作用于主内存的变量,把store操作传来的变量的值放入主内存的变量中。
HashMap在多线程环境下使用需要注意什么?为什么?
Java程序中启动一个线程是用run还是start?
什么是守护线程?有什么用?
什么是死锁?如何避免
答:
由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行,如果线程都不主动释放所占有的资源,将产生死锁 当然死锁的产生是必须要满足一些特定条件的: * 互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 * 请求和保持条件:一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放 * 不剥夺条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 * 循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞
避免死锁的方法:
- 加锁顺序
如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生
- 加锁时限
在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求
- 死锁检测
每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。
检测到死锁,释放所有锁,回退,并且等待一段随机的时间后重试。
- 对部分资源加锁
如何实现一个线程安全的数据结构
答:
- 常量,不需要线程安全
- 方法的局部变量,不需要线程安全;
- 类的成员变量,如果是单例模式,需要;如果不是,则不需要;
- 静态变量,需要线程安全;
编写线程安全类的几个原则:
- 无状态
- 无共享状态
- 使用消息传递
- 使用不变状态
- 使用java.util.concurrent包中的数据结构
- 使用同步块
- 将变量声明为volatile
Java里面的Threadlocal是怎样实现的?
ConcurrentHashMap的实现原理是?
sleep和wait区别
notify和notifyAll区别
volatile关键字的作
ThreadLocal的作用与实现
两个线程如何串行执行
上下文切换是什么含义
可以运行时kill掉一个线程吗?
synchronized关键字作用以及怎么使用?
synchronized 关键字的实现原理?
答:
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
- 当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。
- 然而,当一个线程访问object的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该object中的非synchronized(this)同步代码块。
- 尤其关键的是,当一个线程访问object的一个synchronized(this)同步代码块时,其他线程对object中所有其它synchronized(this)同步代码块的访问将被阻塞。
- 第三个例子同样适用其它同步代码块。也就是说,当一个线程访问object的一个synchronized(this)同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。
- 以上规则对其它对象锁同样适用.
程序计数器会不会发生内存溢出?为什么?
答:不会