本文是作者整理的个人笔记,文中可能引用到其他人的成果但是未指明出处,如有不妥,请指正,谢谢!
转载注明:http://blog.csdn.net/u012294820/article/details/78664716
基础
Object类的常用方法。
(
1
)
clone
保护方法,实现对象的浅拷贝,只有实现了
Cloneable
接口才可以调用该方法,否则抛出
CloneNotSupportedException
异常
(
2
)
equals
在
Object
中与
==
是一样的,子类一般需要重写该方法,重写的同时需要重写hashCode
(
3
)
hashCode
该方法用于哈希查找,重写了
equals
方法一般都要重写
hashCode
方法。这个方法在一些具有哈希功能的
Collection
中用到
(
4
)
getClass
final
方法,获得运行时类型
(
5
)
wait
必须在synchronized块中调用,否则会报异常。调用某个对象的wait()方法,当前线程释放对象锁,暂停执行进入等待状态,不用等前一个线程同步块执行完,下一个线程就可以获得对象锁。
释放锁
给其他等待锁的线程。使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()
方法一直等待,直到获得锁或者被中断。wait(long timeout)
设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生:
- 其他线程调用了该对象的notify方法
- 其他线程调用了该对象的notifyAll方法
- 其他线程调用了interrupt中断当前线程
- 时间间隔到了
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException
异常
(
6
)
notify
必须在synchronized块中调用,否则会报异常。调用某个对象的notify()方法,当前线程释放对象锁,并唤醒一个正在等待该对象锁的线程,只有前一个线程同步块执行完,下一个线程才会获得对象锁。
(
7
)
notifyAll
必须在synchronized块中调用,否则会报异常。调用某个对象的notify()方法,当前线程释放对象锁,并唤醒所有正在等待该对象锁的线程,只有前一个线程同步块执行完,下一个线程才会获得对象锁。
(
8
)
toString
转换成字符串,一般子类都有重写,否则打印句柄
(9)finallize
(1)finalize的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize()对该对象做一下标记需要清除,但是只有在下一次垃圾收集过程中,才会真正回收对象的内存。
(2)finalize()在什么时候被调用?
1.所有对象被Garbage Collection时自动调用,比如运行System.gc()的时候。
2.程序退出时为每个对象调用一次finalize方法。
3.显式的调用finalize方法
clone方法的深拷贝和浅拷贝的区别
浅拷贝
:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,浅复制仅仅复制所考虑的对象,而不复制它所引用的对象。
深拷贝
:被复制对象的所有变量都含有与原来的对象相同的值,除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,深复制把要复制的对象所引用的对象都复制了一遍。
彻底的深拷贝
:实现Serializable接口,将对象写到流中,并从流中读取重建对象,前提是该对象以及它内部引用的对象必须可序列化,否则不可序列化的对象必须设为transient;如果序列化的对象字段有改动,反序列化时会抛出异常,所以需要设置serialVersionUID属性,在反序列化过程中会判断serialVersionUID的值是否一致,新添加或更改的字段值将设为初始化值(对象为null,基本类型为相应的初始默认值),字段被删除将不设置
参考一:
Java对象的深复制和浅复制
wait操作和sleep的操作的区别
①
这两个方法来自不同的类分别是,sleep
来自Thread
类,和wait
来自Object
类。
sleep
是Thread
的静态类方法,
谁调用的谁去睡觉,即使在
a
线程里调用
b
的
sleep
方法,实际上还是
a
去睡觉,要让
b
线程睡觉要在
b
的代码中调用
sleep
。
②
锁:
最主要是sleep
方法没有释放锁,而wait
方法释放了锁,使得其他线程可以使用同步控制块或者方法。
sleep
不出让系统资源;wait
是进入线程等待池等待,出让系统资源,其他线程可以占用CPU
。一般wait
不会加时间限制,因为如果wait
线程的运行资源不够,再出来也没用,要等待其他线程调用notify/notifyAll
唤醒等待池中的所有线程,才会进入就绪队列等待OS
分配系统资源。sleep(milliseconds)
可以用时间指定使它自动唤醒过来,如果时间不到只能调用interrupt()
强行打断。
Thread.sleep(0)
的作用是“触发操作系统立刻重新进行一次CPU
竞争”。
③
使用范围:wait
,notify
和notifyAll
只能在同步控制方法或者同步控制块里面使用,而sleep
可以在任何地方使用。
synchronized(x){
x.notify()
//
或者wait()
}
那你说说多线程之间的通信,回答用wait sleep notify notifyAll配合使用 然后就问 wait和sleep一样吗?回答不一样
Object:
wait、notify、notifyAll
1.wait():当前线程等待,释放锁
2.notify():当前线程释放锁,唤醒等一个等待该锁的线程
3.notifyAll():当前线程释放锁,唤醒等所有等待该锁的线程
Thread:
sleep、join、yield
1.sleep():使当前线程(即调用该方法的线程)暂停执行一段时间,让其他线程有机会继续执行,但它并不释放对象锁。也就是说如果有synchronized同步块,其他线程仍然不能访问共享数据。注意该方法要捕捉异常。可以使优先级低的线程得到执行的机会。
2.join():等待当前线程死亡,如果当前线程未死亡,则程序就会一直阻塞在那。(当前线程未死亡,则循环调用wait(0))
3.yield():让步,建议当前线程将cpu资源让给具有相同优先级的线程(包括当前线程自己),但是不能保证其他线程一定可以得到cpu资源。
java如何实现多态
多态就是同一个接口,使用不同的实例而执行不同操作
通过重载和重写
重载:方法名要一样,但是参数类型和个数不一样,返回值类型可以相同也可以不相同,无法以返回类型作为重载函数的区分标准。
1、必须具有不同的参数列表;
2、可以有相同的返回类型,只要参数列表不同就可以了;
3、可以有不同的访问修饰符;
4、可以抛出不同的异常;
重写:重写父类的方法,子类函数的访问修饰权限不能小于父类的权限
1、参数列表必须完全与被重写的方法相同,否则不能称其为重写而是重载。
2、返回的类型必须一直与被重写的方法的返回类型相同,否则不能称其为重写而是重载。
3、访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)
4、重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常。例如:
父类的一个方法申明了一个检查异常IOException,在重写这个方法是就不能抛出Exception,只能抛出IOException的子类异常,可以抛出非检查异常。
泛型怎么实现的
含义:泛型的本质是
参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为
泛型类、泛型接口、泛型方法
特性:在编译阶段进行类型检查,之后进行
类型擦除(运行时List<String>和List<Integer>的类型都是List)
泛型模式:
K ——键,比如映射的键。
V ——值,比如 List 和 Set 的内容,或者 Map 中的值。
E ——异常类。
T ——泛型。
泛型通配符:?,?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类,
当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能,那么可以用 "?" 通配符来表未知类型
泛型方法:public <T> void printMsg( T arg)
Java内存溢出和内存泄露
内存泄露:指程序中动态分配内存给一些临时对象,但是对象不会被GC所回收,它始终占用内存。即
被分配的对象可达但已无用。
内存溢出:指程序运行过程中
无法申请到足够的内存而导致的一种错误。内存溢出通常发生于OLD段或Perm段垃圾回收后,仍然无内存空间容纳新的Java对象的情况。
从定义上可以看出内
存泄露是内存溢出的一种诱因,不是唯一因素。
内存泄漏的情况:
1、长生命周期的对象持有短生命周期对象的引用
在全局静态map中缓存局部变量,且没有清空操作,随着时间的推移,这个map会越来越大,造成内存泄露。
2、修改hashset中对象的参数值,且参数是计算哈希值的字段
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些
参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。
3、机器的连接数和关闭时间设置
长时间开启非常耗费资源的连接,也会造成内存泄露。数据库连接、网络连接、IO流连接未关闭
内存溢出的情况:
1、堆内存溢出(
outOfMemoryError:java heap space)
在jvm规范中,堆中的内存是用来生成对象实例和数组的。
如果细分,堆内存还可以分为年轻代和年老代,年轻代包括一个eden区和两个survivor区。
当生成新对象时,内存的申请过程如下:
a、jvm先尝试在eden区分配新建对象所需的内存;
b、如果内存大小足够,申请结束,否则下一步;
c、jvm启动youngGC,试图将eden区中不活跃的对象释放掉,释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
d、Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
e、 当OLD区空间不够时,JVM会在OLD区进行full GC;
f、full GC后,若Survivor及OLD区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”out of memory错误”:outOfMemoryError:java heap space
2、方法区内存溢出(
outOfMemoryError:permgen space)
在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。
所以如果程序
加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为:outOfMemoryError:permgen space
3、线程栈溢出(
java.lang.StackOverflowError)
线程栈是线程独有的一块内存结构,所以线程栈发生问题必定是某个线程运行时产生的错误。
一般线程栈溢出是由于
递归太深或方法调用层级过多导致的。
发生栈溢出的错误信息为:java.lang.StackOverflowError
Integer和int,拆箱和装箱
自动装箱
例如:Integer i = 100;
相当于编译器自动为您作以下的语法编译:Integer i = Integer.valueOf(100);
自动拆箱
自动拆箱,也就是将对象中的基本数据从对象中自动取出。如下可实现自动拆箱:
1 Integer i = 10; //装箱
2 int t = i; //拆箱,实际上执行了 int t = i.intValue();
内部类和静态内部类
比较Java和C++
(1)Java比C++程序可靠性更高。
(2)Java语言不需要程序对内存进行分配和回收。
(3)Java语言中没有指针的概念,引入了真正的数组。
(4)Java用接口(Interface)技术取代C++程序中的多继承性。
序列化的好处与原理,transient
实现方式:实现Serializable或者Externalizable接口
好处:可以完全还原对象
应用场景:1)网络中传递对象,2)将对象保存在硬盘中,用于服务器重启恢复对象状态
原理:将对象转为字节
transient:忽略字段,不参与序列化
参考一:
Java序列化机制和原理
=============================================================================
JVM
jvm内存模型
方法区:类加载后的方法、变量引用等数据
堆:系统几乎所有的对象都在此区域
虚拟机栈:Java方法执行的内存模型
本地方法栈:Native方法执行的内存模型
程序计数器:记录当前线程所执行的字节码的行号
参考一:
JVM内存模型
Java内存管理
(1)请描述java的内存管理原理
(2)请描述java的内存分区
(3)请描述java的对象生命周期,以及对象的访问?
java的垃圾回收机制
Java中要对无用对象进行两次标记才会回收内存。
垃圾回收算法
1.
引用计数算法
:当对象被引用时,引用计数器加1,相反减1,缺点是无法解决相互引用的问题。
2.
标记-清除算法
:标记所有从根节点开始的可达对象,清除所有未被标记的对象。(适用于老年代)
3.
复制算法
:将内存空间分成两块,每次将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块。算法效率高,但是代价是将系统内存折半。(适用于新生代。存活对象少,垃圾对象多)
4.
标记-压缩算法(也叫标记-整理)
:该算法是对“标记-清除算法”的改进,不是直接对标记对象进行清除,而是将存活的对象压缩到内存的一端,然后直接清理掉边界以外的内存。(适用于老年代)
5.
分代算法
:根据对象的存活周期的不同将内存划分为几块,每块视为一代,一般是把java内存堆分为新生代、老年代和永久代(HotSpot中才有永久代)。根据各个年代的特点采用最适当的垃圾收集算法。
判断对象是否存活:主流的Java虚拟机没有选用引用计数算法(Python使用 )来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。Java(包括C#)都是通过可达性分析算法来判断对象是否存活的。
分代算法(针对HotSpot jvm)
年轻代:
复制算法,Eden(8)、From(1)、To(1)
- HotSpot将新生代划分为三块,一块较大的Eden空间和两块较小的Survivor空间,默认比例为8:1:1。是因为HotSpot采用复制算法来回收年轻代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在Eden区分配(大对象直接进入老年代),当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。
- GC开始时,对象只会存在于Eden区和From Survivor区,To Survivor区是空的(作为保留区域)。GC进行时,Eden区中所有存活的对象都会被复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到年龄阀值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1,GC分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着, From Survivor区和To Survivor区会交换它们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
老年代:
标记-清除算法
- 年轻代中经历了一定次数的GC的对象进入老年代
- 新生成的大对象直接进入老年代
永久代:存放类信息、常量、静态变量等,也就是方法区。
Minor GC 和 Full GC的区别:
- 新生代GC(Minor GC):Minor GC指发生在年轻代的GC,因为年轻代的Java对象大多都是朝生夕死,所以Minor GC非常频繁,一般回收速度也比较快。当Eden空间不足以为对象分配内存时,会触发Minor GC。
- 老年代GC(Full GC/Major GC):Full GC指发生在年轻代、老年代、永久代(如果存在的话),出现了Full GC一般会伴随着至少一次的Minor GC(老年代的对象大部分是Minor GC过程中从年轻代进入老年代),比如:分配担保失败。Full GC的速度一般会比Minor GC慢10倍以上。当老年代内存不足或者显式调用System.gc()方法时,会触发Full GC。
参考一:
Java虚拟机:JVM内存分代策略
是否了解堆外内存?
不受JVM控制内存,直接操作系统内存。
堆外内存可以通过java.nio的ByteBuffer来创建,调用allocateDirect方法申请即可
说说safepoint,又说到了安全区
从线程角度看,safepoint可以理解成是在代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要,可以在这个位置暂停,比如发生GC时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待GC结束。
给你一个64G的内存,你会怎么设置堆大小?
CMS垃圾收集器
- Concurrent Mark Sweep;
- CMS是一款并发、使用标记-清除算法的gc;
- CMS是针对老年代进行回收的GC。
特点:
- CMS以获取最小停顿时间为目的,牺牲了吞吐量。
- 在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。
执行过程
- 初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的“根对象”开始,只扫描到能够和“根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。暂停所有活动。
- 并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。GC Roots Tracing
- 并发预清理:并发预清理阶段仍然是并发的。在这个阶段,虚拟机查找在执行并发标记阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代)。通过重新扫描,减少下一个阶段“重新标记”的工作,因为下一个阶段会Stop The World。
- 重新标记:这个阶段会暂停虚拟机,收集器线程扫描在CMS堆中剩余的对象。扫描从“根对象”开始向下追溯,并处理对象关联。暂停所有活动。修正标记改变的对象的标记记录。
- 并发清理:清理垃圾对象,这个阶段收集器线程和应用程序线程并发执行。
- 并发重置:这个阶段,重置CMS收集器的数据结构,等待下一次垃圾回收。
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。其他动作都是并发的。
缺点:
- CMS回收器采用的基础算法是Mark-Sweep。所有CMS不会整理、压缩堆空间。这样就会有一个问题:经过CMS收集的堆会产生空间碎片。 CMS不对堆空间整理压缩节约了垃圾回收的停顿时间,但也带来的堆空间的浪费。为了解决堆空间浪费问题,CMS回收器不再采用简单的指针指向一块可用堆空 间来为下次对象分配使用。而是把一些未分配的空间汇总成一个列表,当JVM分配对象空间的时候,会搜索这个列表找到足够大的空间来hold住这个对象。
- 需要更多的CPU资源。从上面的图可以看到,为了让应用程序不停顿,CMS线程和应用程序线程并发执行,这样就需要有更多的CPU,单纯靠线程切 换是不靠谱的。并且,重新标记阶段,为空保证STW快速完成,也要用到更多的甚至所有的CPU资源。当然,多核多CPU也是未来的趋势!
- 需要更大的堆空间。因为CMS标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在CMS回 收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。也就是说,CMS不会在老年代满的时候才开始收集。相反,它会尝试更早的开始收集,已避免上面提到的情况:在回收完成之前,堆没有足够空间分配!默认当老年代使用68%的时候,CMS就开始行动了。 – XX:CMSInitiatingOccupancyFraction =n 来设置这个阀值
参考一:
CMS垃圾回收机制
G1垃圾收集器
执行过程:
- 初始标记:在这个阶段,需要虚拟机停顿正在执行的任务,官方的叫法STW(Stop The Word)。这个过程从垃圾回收的“根对象”开始,只扫描到能够和“根对象”直接关联的对象,并作标记。所以这个过程虽然暂停了整个JVM,但是很快就完成了。暂停所有活动。
- 并发标记:这个阶段紧随初始标记阶段,在初始标记的基础上继续向下追溯标记。并发标记阶段,应用程序的线程和并发标记的线程并发执行,所以用户不会感受到停顿。GC Roots Tracing
- 最终标记:修正在并发标记期间因用户线程继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这个阶段需要停顿线程,但是可以并行执行。暂停所有活动。
- 筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
关于Region概念:
关于Remembered Set概念:G1收集器中,Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用是使用Remembered Set来避免扫描全堆。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序对Reference类型数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之间(在分代中例子中就是检查是否老年代中的对象引用了新生代的对象),如果是便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当内存回收时,在GC根节点的枚举范围加入Remembered Set即可保证不对全局堆扫描也不会有遗漏。
参考一:
JVM中的G1垃圾回收器
Partial GC:并不收集整个GC堆的模式
- Young GC:只收集young gen的GC
- Old GC:只收集old gen的GC。只有CMS的concurrent collection是这个模式
- Mixed GC:收集整个young gen以及部分old gen的GC。只有G1有这个模式
Full GC:收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。
jvm虚拟内存分布
参考一:
触摸java常量池
java编译、加载、执行过程,类加载机制,怎么破坏单例(这里应该想让我用类加载机制来破坏)?
类的生命周期
=============================================================================
多线程
为什么一个空的死循环会让CPU占用达到100%
CPU占用率:现代操作系统对CPU是按照时间分片进行的。比如A进程占用10ms,然后B进程占用30ms,然后空闲60ms,再又是A进程占10ms,B进程占30ms,空闲60ms;如果在一段时间内都是如此,那么这段时间内的占用率为40%。
程序在等待或阻塞的时候,是不占用CPU的时间的。
空的死循环:一个空的死循环不会做任何事情,但是会不断向CPU申请时间片,直到把每个时间片都占用完。这样CPU就没有空余的时间片来做其他有用的事情了。
注:Thread.sleep是阻塞操作,它是不占用CPU时间的,与死循环不一样
性能优化中CPU、内存、磁盘IO、网络性能的依赖
IO等待占用cpu
CPU:用于计算
IO:输入输出设备。磁盘IO、键盘IO、网络IO
多线程提升性能
1)多线程的应用不是为了提高运行效率,而是为了提高资源使用效率
2)多线程编程的目的,就是"最大限度地利用CPU资源"
3)采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的响应时间
synchronized的原理
通过monitorenter和monitorexit指令实现
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1.
修饰一个代码块
,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象; synchronized(this){...}
2.
修饰一个方法
,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象; public synchronized
3.
修饰一个静态的方法
,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4.
修饰一个类
,其作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。
volatile(一般线程并发时使用)
一个变量修饰符,一旦一个共享变量,被volatile修饰之后,就具备了两种特性:
(1)
保证此变量对所有线程的可见性,即一个线程修改了这个变量的值,这新值对其他线程来说是立即可见的。由于Java里面的运算不是原子操作,导致volatile变量的运算在并发下一样是不安全的,主线程中无法获取最新值,通过Javap反编译代码得到代码清单,查看字节码可以证实。所以多线程下计数器必须使用锁保护,如下图,我们期望race=200000,但是实际race<200000,如果给increase()加锁,即添加synchronized同步修饰符,race=200000
由于volatile变量只能保证可见性,我们可以通过加锁(使用synchronized或者java.util.concurrent中的原子类)来保证原子性。或者满足以下两条规则的运算场景:(1)运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值(2)变量不需要与其他的状态变量共同参与不变约束。
(2)
禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点。即在线程并发的情况下,由于机器级的优化操作导致靠后的汇编代码提前执行,如下图,如果变量initialized没有被volatile修饰,那么线程A中代码“initialized = true”会在配置文件读取之前执行,此时轮到线程B执行,那么initialized的值对于线程B来说是不正确的,使用volatile则可以避免这种情况。通过查看代码清单,我们可以发现禁止指令重排序优化是通过设置内存屏障(多执行一个lock操作,防止提前执行后面的指令)实现的。
volatile只提供了保证访问该变量时,每次都是从内存读取最新值,并不会使用寄存器缓存该值,每次都会从内存中读取。而对该变量的修改,volatile并不提供原子性的保证。
由于及时更新,很可能导致另一线程访问最新变量值,无法跳出循环的情况
参考一:
你真的了解volatile关键字吗?
synchronized和lock的区别
1、
lock功能更强大。ReentrantLock拥有Synchronized相同的并发性和内存语义,此外还多了 锁投票,定时锁等候和中断锁等候
线程A和B都要获取对象O的锁定,假设A获取了对象O锁,B将等待A释放对O的锁定,
如果使用 synchronized ,如果A不释放,B将一直等下去,不能被中断
如果 使用ReentrantLock,如果A不释放,可以使B在等待了足够长的时间以后,中断等待,而干别的事情
ReentrantLock获取锁定与三种方式:
a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
c) tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断
2、
lock需要主动释放。synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将unLock()放到finally{}中
3、
竞争激烈,lock性能更好。在资源竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
Java怎么保证原子性
1、synchronized
2、CAS操作,比如:
AtomicInteger atomicI = new AtomicInteger(0); atomicI.compareAndSet(i, ++i);
java多线程,项目中有没有用到多线程
1、多线程操作文件
2、servlet多线程
3、后台定时任务
4、吞吐量
参考一:
多线程的应用场景
新建线程的几个方法
1、Thread
2、Runnable
3、Callable和FutureTask
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)
调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
Thread、Executor、ForkJoin和Actor
=============================================================================
对比
String
、
StringBuffer
、
StringBuilder
String
字符串常量
String
是不可变的对象,
每次对 String
类型进行改变时都会生成了一个新的 String
对象,然后将指针指向新的 String
对象,经常改变String
会生成许多无引用对象,触发JVM
的 GC
开始工作,影响程序执行的性能。
StringBuffer
字符串变量(线程安全)
StringBuffer
类每次修改都是对 StringBuffer
对象本身进行操作(JVM内部生成一个StringBuilder进行操作
),不会生成新的对象。
多线程
需要经常修改字符串值的情况下推荐使用 StringBuffer
。
StringBuilder
字符串变量(非线程安全)
StringBuilder
类每次修改都是对 StringBuilder
对象本身进行操作,不会生成新的对象。
单线程
中需要经常修改字符串值的情况下推荐使用 StringBuilder
。
Map、Set、List、Queue、Stack
Map
Iterable<-
Collection<-
List<-Vector<-
Stack
<-
Set
<-
Queue
Map(TreeMap保证顺序,效率低;Hash不保证顺序,效率高)
键映射到值的对象。键不能重复,键值对映射关系是一对一。
某些映射实现可明确保证其顺序,如 TreeMap 类;另一些映射实现则不保证顺序,如 HashMap 类。
Map中元素,可以将key序列、value序列单独抽取出来。 使用keySet()抽取key序列,将map中的所有keys生成一个Set。 使用values()抽取value序列,将map中的所有values生成一个Collection。 为什么一个生成Set,一个生成Collection?那是因为,key总是独一无二的,value允许重复。
Set
一个
不包含重复元素
的 collection。更确切地讲,set 不包含满足 e1.equals(e2) 的元素对 e1 和 e2,并且最多包含一个 null 元素。
不可随机访问包含的元素
只能用Iterator实现单向遍历
Set 没有同步方法
List
可随机访问包含的元素,元素是有序的,可在任意位置增、删元素,不管访问多少次,元素位置不变,允许重复元素 用Iterator实现单向遍历,也可用ListIterator实现双向遍历
Queue
先进先出
Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用
offer()
来加入元素,使用
poll()
来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。 如果要使用前端而不移出该元素,使用element()或者peek()方法。 值得注意的是LinkedList类实现了Queue接口,因此我们可以把LinkedList当成Queue来用。
Queue 实现通常不允许插入 null 元素,尽管某些实现(如 LinkedList)并不禁止插入 null。即使在允许 null 的实现中,也不应该将 null 插入到 Queue 中,因为 null 也用作 poll 方法的一个特殊返回值,表明队列不包含元素。
add-remove:操作失败会
抛出异常。
- add:返回bool值
- remove:移除头部元素,并返回该元素
offer-poll:通过
返回值判断操作是否成功。BlockingQueue接口也有阻塞机制
- offer:
- poll:
put-take:操作不成功则
阻塞。BlockingQueue接口中才有这两个方法
- put:
- take:
element-peek:返回头部元素。
- element:返回队列头部的元素,如果队列为空,则抛出NoSuchElementException异常
- peek:返回队列头部的元素
Stack(线程安全)
后进先出
Stack继承自Vector(可增长的对象数组),也是同步的,它通过五个操作对类 Vector 进行了扩展 ,允许将向量视为堆栈。它提供了通常的
push
和
pop
操作,以及取堆栈顶点的 peek 方法、测试堆栈是否为空的 empty 方法、在堆栈中查找项并确定到堆栈顶距离的 search 方法。
- 用法
如果涉及到堆栈、队列等操作,应该考虑用List;
对于需要快速插入,删除元素,应该使用LinkedList;
如果需要快速随机访问元素,应该使用ArrayList。
如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高
HashMap
、
ConcurrentHashMap、WeekHashMap
HashMap
Java7,数组+链表
多线程环境下,HashMap进行
put操作会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry
Java8,数组+链表+红黑树
当一个桶中链表的长度大于8时,链表变为红黑树
总结
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
HashMap和HashTable边遍历边删除会报ConcurrentModificationException异常,必须使用Iterator进行遍历删除,但是ConcurrentHashMap不会
ConcurrentHashMap
Java7
Segment
+
HashEntry
ConcurrentHashMap
的
锁分段技术
:假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap
所使用的锁分段技术。首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。用一个Segment
数组维护所有的键值对,一个Segment
对象的数据结构相当于一个HashMap
ConcurrentHashMap
不允许Key
或者Value
的值为NULL
JDK8
- Node + CAS(Compare and Swap) + Synchronized,具体使用compareAndSwapInt无锁原子方法
- 取消segments字段,直接采用transient volatile Node<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。
- 将原先 table数组+单向链表 的数据结构,变更为 table数组+单向链表+红黑树 的结构。对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
使用总结
WeekHashMap
HashMap
、
LinkedHashMap、TreeMap
HashMap
、
HashSet
TreeMap、TreeSet
集合框架肯定经常用吧?你知道HashMap的数据结构和实现原理吧?
数组和链表的结合体,顺便谈了下底层数组Entry,实现原理这块,主要是说了下hash、put和get的实现过程,以及当遇到空值以及当两个键有相同hashcode值的处理
ConcurrentHashMap和HashMap的底层实现,处理冲突
ConcurrentHashMap和SynchronizedMap的区别以及效率问题
区别:
1、ConcurrentHashMap分段锁,对每个桶加锁
2、SynchronizedMap会锁住整个对象,每次读写都会加锁,每次最多一个线程操作
效率:ConcurrentHashMap效率高
HashMap和HashTable的区别,HashMap的扩容方法。前者为什么不安全,哪里不安全,什么情况下不安全。前者底层怎么实现的,画一画。这块问的比较详细。
哈希表一般使用“拉链法”实现,即存储链表的数组
1、HashMap是非线程安全的,HashTable是线程安全的。
2、HashMap的键和值都允许有null值存在,而HashTable则不行。
3、因为线程安全的问题,HashMap效率比HashTable的要高。
安全性:HashMap多线程读写、写写不安全,HashTable对每个方法加上锁synchronized。
HashMap的扩容方法:加载因子0.75,初始容量16,每次扩容2倍(当元素个数达到加载因子*当前容量时,进行扩容,所以实际容量为当前容量的0.75倍)
ArrayList、Linkedlist和Vector的区别以及各自的试用场景,以及ArrayList的扩容方法。
Arraylist:数组存储,查询快,插入删除慢
Vector:数组存储,内部使用synchronized保证线程安全,性能差
LinkedList:双向链表存储,可以前后双向遍历,插入和删除数据较快。
ArrayList的扩容:
int
newCapacity = (oldCapacity * 3)/2 + 1; 扩容为原来的1.5倍
Collection类,list和set里面都有什么,有什么区别
1、Set不允许重复,List允许重复
2、Set没有顺序,List有顺序
Java阻塞队列
BlockingQueue接口
- ArrayBlockingQueue:ArrayBlockingQueue 是一个有界的阻塞队列,数组存储。初始化的时候可以设置容量,但不能动态扩容。
- DelayQueue:DelayQueue 对元素进行持有直到一个特定的延迟到期。注入其中的元素必须实现 java.util.concurrent.Delayed 接口。
- LinkedBlockingQueue:LinkedBlockingQueue 内部以一个链式结构(链接节点)对其元素进行存储。可以设置容量上限。默认使用 Integer.MAX_VALUE 作为上限。
- PriorityBlockingQueue:PriorityBlockingQueue 是一个无界的并发队列。它使用了和类 java.util.PriorityQueue 一样的排序规则。不能插入 null 值。所有插入到 PriorityBlockingQueue 的元素必须实现 java.lang.Comparable 接口。因此该队列中元素的排序就取决于你自己的 Comparable 实现。
- SynchronousQueue:SynchronousQueue 是一个特殊的队列,它的内部同时只能够容纳单个元素。如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插入了一条新的元素。
hash冲突有哪些解决方法?简单说下再哈希法?怎么保证多个hash函数不会出现死循环?(很懵逼)
解决办法:
1.
开放地址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
2.
再哈希法。这种方法是同时构造多个不同的哈希函数,直到hash值不重复
3.
链地址法(Java hashmap就是这么做的)。链表存储冲突的元素
4.
建立一个公共溢出区。发生冲突的元素存入公共溢区(也是一张hash表)
String占多大内存?
=============================================================================
Java异常机制
Excption
与
Error
。
OOM
你遇到过哪些情况,
SOF
你遇到过哪些情况。
(
1
)
Java
异常
Java
中有Error
和Exception
,它们都是继承自Throwable
类。
二者的不同之处
Exception
:
- 可以是可被控制(checked) 或不可控制的(unchecked)。
- 表示一个由程序员导致的错误。
- 应该在应用程序级被处理。
Error
:
- 总是不可控制的(unchecked)。
- 经常用来用于表示系统错误或低层资源的错误。
- 如果可能的话,应该在系统级被捕捉。
异常的分类
- Checked exception: 这类异常都是Exception的子类。异常的向上抛出机制进行处理,假如子类可能产生A异常,那么在父类中也必须throws A异常。可能导致的问题:代码效率低,耦合度过高。
- Unchecked exception: 这类异常都是RuntimeException的子类,虽然RuntimeException同样也是Exception的子类,但是它们是非凡的,它们不能通过client code来试图解决,所以称为Unchecked exception。
如何触发OOM异常
1、堆内存溢出(
outOfMemoryError:java heap space)
创建对象过多,比如:死循环中list不断添加元素
2、方法区内存溢出(
outOfMemoryError:permgen space)
在jvm规范中,方法区主要存放的是类信息、常量、静态变量等。
所以如果程序
加载的类过多,或者使用反射、gclib等这种动态代理生成类的技术,就可能导致该区发生内存溢出,一般该区发生内存溢出时的错误信息为:outOfMemoryError:permgen space
如何触发SOF异常
1、线程栈溢出(
java.lang.StackOverflowError)
一般线程栈溢出是由于
递归太深或方法调用层级过多导致的。比如:无限递归
发生栈溢出的错误信息为:java.lang.StackOverflowError
=============================================================================
Java缓存实现
LRU缓存实现
通过继承LinkedHashMap实现。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int MAX_CACHE_SIZE;
public LRUCache(int cacheSize) {
// (cacheSize / 0.75)是为了使容量达到cacheSize;(+1)是为了防止达到cacheSize时触发扩容
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
MAX_CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
}