编译阶段:检查java源程序是否符合Java语法,符合Java语法贼能够生成正常的字节码文件(.class)
javac使用规则:javac xxx.java
值得注意的是,在java语言里,类型的加载和连接过程都是在程序运行期间完成的。虽然在加载时增加了一些性能开销,但是也让java语言拥有了可以动态扩展的语言特性。
运行阶段过程:
类从被加载到虚拟机内存中开始,到卸载出内存为止。它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,如下图所示。
jvm将.class类文件信息加载到内存并解析成对应的class对象的过程,注意:jvm并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次.
主要分为三部分(五步):1、加载,2、链接(1.验证,2.准备,3.解析),3、初始化
- 加载
类加载器包括 BootClassLoader、ExtClassLoader、APPClassLoader- 连接(包含验证、准备、解析)
验证:(验证class文件的字节流是否符合jvm规范)
准备:为类变量分配内存,并且进行赋初值
解析:将常量池里面的符号引用(变量名)替换成直接引用(内存地址)过程,在解析阶段,jvm会把所有的类名、方法名、字段名、这些符号引用替换成具体的内存地址或者偏移量。- 初始化
主要对类变量进行初始化,执行类构造器的过程,简单点说,只对static修饰的变量或者语句进行初始化。
验证是虚拟机对自身保护的一项重要工作。确保Class文件的字节流中包含的信息符合当前虚拟机的要求。Java语言是相对安全的语言(相对于C/C++来看),他做不到一些事,比如访问数组边界以外的数据,如果这样做,编译器将拒绝编译。但是Class不一定要求用Java源码编译而来,可以使用任何途径。所以必须要验证。
分配内存;设置初始值。(值得注意的是分配内存和设置初始值的不是全部变量)
符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用的目标必定已经在内存中存在。
虚拟机中对于初始化阶段,严格规定以下情况必须立即对类进行"初始化"(而加载、验证、准备自然在此之前开始):
new关键字实例化对象;读取、设置一个类的静态变量(被final修饰、已在编译期把结果放入常量池的静态字段除外);调用一个类的静态方法。
Java语言流行的重要原因之一。
对于任意一个类,都需要由加载他的类加载器和它本身一同确立其在Java虚拟机中的唯一性。这里相等包括Class对象的一些方法(比如equals)还有instanceof关键字等。
从Java虚拟机的角度看,只有2种类加载器,C++语言实现的启动类加载器(Bootstrap ClassLoader),是虚拟机自身的第一部分。另一类就是其他类加载器,独立于虚拟机外部。(如Extension CLassLoader和Application ClassLoader)
当一个ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器Bootstrap CLassLoader进行加载。
要求除顶层启动加载器(Bootstrap ClassLoader)之外,其余的类加载器都应该有自己的父类加载器(不是继承关系,是组合关系系)。
Java类随着他的类加载器一起具备了一种带有优先级的层次关系。对于保障Java程序的稳定运作起了重要作用。
拿java.lang.Object来说,你加载它经过一层层委托最终是由Bootstrap ClassLoader来加载的,也就是最终都是由Bootstrap ClassLoader去找<JAVA_HOME>\lib中rt.jar里面的java.lang.Object加载到JVM中。
实现双亲委派的代码在java.lang.ClassLoader的loadClass()方法,逻辑清晰易懂:先检查是否已被加载过,没有则调用父加载器的loadClass(),若父加载器为空,则默认使用启动类加载器作为父加载器。父类加载失败,抛出ClassNotFoundException异常后调用自己的findClass()方法加载,自己也找不到,ClassNotFoundException就正儿八经的抛出了。
双亲委派模型不是强制性约束模型,Java世界里大多数类加载器都遵循这个模型,但是也有意外。
StringBuffer里面的很多方法添加了synchronized关键字,是可以表征线程安全的,所以多线程情况下使用它。
执行速度:StringBuilder > StringBuffer > String
StringBuilder牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而String是字符串常量(final)修试,另外两个是字符串变量。
提示:不要使用String拼接,这样会频繁回收新建,使用另外2个通过append方法添加比较合适。
首先要搞清楚 基本类型 和 引用类型。
//基本类型num
int num = 10;
//引用类型str
String str = "hello";
再来说赋值运算符
//直接改变变量的值,原来的值被覆盖掉
num = 20;
//改变引用中所保存的地址,原来的地址被覆盖掉。但是原来的对象不会被改变
str = "java";
//arr是引用
//stack上仅仅占用4字节空间,new int[10]会在heap中开辟一个数组对象,然后arr指向它。
int[] arr = new int[10]
二维数组
int[][] arr2 = new int[2][4]
分析:arr2同样仅在stack中占用4个字节,会在内存中开辟一个长度为2的,类型为int[]的数组,然后arr2指向这个数组。这个数组内部有两个引用(大小为4字节),分别指向两个长度为4的类型为int的数组。
实际上String对象内部仅需要维护三个变量,char[] chars, int startIndex, int length。所以上面关于String的例子是简化版的。String被设计成为了不可变类型。
总结:不用纠结于概念,其实区别在于操作的是一块内存还是新开辟了一块内存的区别。然后Java始终是传值的。
equals方法是超类Object中的一个基本方法,用于检测一个对象是否与另外一个对象相等。而在Object类中这个方法实际上是判断两个对象是否具有相同的引用。
public boolean equals(Object obj) { return (this == obj); }
默认情况下也就是从超类Object继承而来的equals方法与‘==’是完全等价的,比较的都是对象的内存地址。
需要注意hashCode方法
object类的equals函数的API,里面有这样一句话:
注意:当此方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode
方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
在java中,我们可以使用hashCode()来获取对象的哈希码,其值就是对象的存储地址,这个方法在Object类中声明,因此所有的子类都含有该方法。
hashCode的意思就是散列码,也就是哈希码,是由对象导出的一个整型值,散列码是没有规律的。
String s="ok";
String ss="ok";
//String t = new String("ok");
String t = new String(s);
StringBuilder sb =new StringBuilder(s);
System.out.println(s.hashCode()+" "+ss.hashCode()+" "+t.hashCode()+" "+sb.hashCode());
System.out.println(s==t);
System.out.println(s.equals(t));
System.out.println(s==ss);
System.out.println(s.equals(ss));
打印结果
3548 3548 3548 1704856573
false
true
true
true
类比Interger:两个new出来Integer对象,即使值相同,通过“==”比较结果为false,但两个对象直接赋值,则通过“==”比较结果为“true,这一点与String非常相似。
字符串的散列码是由内容导出的,因为String类重写了hashCode方法,所以上述方法s和t的hashCode是一样的,又因为StringBuilder直接使用了超类的hashCode方法,所以sb字段的hashCode与另外2个不同。
在JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference),这四种引用强度依次逐渐减弱。
就是指在程序代码之中普遍存在,类似“Object obj = new Object()”这类的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC能够回收
obj =null;
如果是方法的内部有一个强引用,引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。
如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。
应用场景示例:浏览器的后退按钮。
// 获取浏览器对象进行浏览
Browser browser = new Browser();
// 从后台程序加载浏览页面
BrowserPage page = browser.getPage();
// 将浏览完毕的页面置为软引用
SoftReference softReference = new SoftReference(page);
// 回退或者再次浏览此页面时
if(softReference.get() != null) {
// 内存充足,还没有被回收器回收,直接获取缓存
page = softReference.get();
} else {
// 内存不足,软引用的对象已经回收
page = browser.getPage();
// 重新构建软引用
softReference = new SoftReference(page);
}
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
值得一提的是谷歌官方推荐使用WeakReference。
又称幽灵引用、幻影引用。虚引用顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。
能在这个对象被收集器回收时收到一个系统通知。
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
String str = new String("abc");
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
1. 方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
2. Java堆(heap):存储Java实例或者对象的地方。这块是gc的主要区域。
3. Java栈(stack):Java栈总是和线程关联的,每当创建一个线程时,JVM就会为这个线程创建一个对应的Java栈。在这个java栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在java栈中入栈到出栈的过程。所以java栈是线程私有的。
4. 程序计数器:用于保存当前线程执行的内存地址,由于JVM是多线程执行的,所以为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
5. 本地方法栈:和Java栈的作用差不多,只不过是为JVM使用到的native方法服务的。
垃圾回收(Garbage Collection)是Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。
垃圾回收只会负责释放那些对象占有的内存。对象是个抽象的词,包括引用和其占据的内存空间。当对象没有任何引用时其占据的内存空间随即被收回备用,此时对象也就被销毁。但不能说是回收对象,没错这就是文字游戏。
垃圾:无任何对象引用的对象
回收:清理“垃圾”占用的内存空间而非对象本身
发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中
思考:找到垃圾的算法和回收垃圾算法有哪些?堆内存为配合垃圾回收有什么区域划分?
Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。
新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
以HotSpot虚拟机为例子,默认新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 )。新生代中,E默认den : From Survivor : To Survivor= 8 :1 : 1 ( 可以通过参数–XX:SurvivorRatio来设定 )
引用计数算法(Reference Counting Collector)
堆中每个对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。(比如a=b,b被引用)当引用失效时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。难以检测出对象之间的循环引用。同时,引用计数器增加了程序执行的开销。所以Java语言并没有选择这种算法进行垃圾回收。
早期的JVM使用引用计数,现在大多数JVM采用对象引用遍历(根搜索算法)。
根搜索算法(Tracing Collector)
首先了解一个概念**:根集(Root Set)**
所谓根集(Root Set)就是正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法。
(1)通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。
(2)找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。
(3)重复(2)。
(4)搜索所走过的路径称为引用链,当一个对象到GC。 Roots没有任何引用链相连时,就证明此对象是不可用的。
Java和C#中都是采用根搜索算法来判定对象是否存活的。
垃圾回收器将某些特殊的对象定义为GC根对象。所谓的GC根对象包括:
(1)虚拟机栈中引用的对象(栈帧中的本地变量表);
(2)方法区中的常量引用的对象;
(3)方法区中的类静态属性引用的对象;
(4)本地方法栈中JNI(Native方法)的引用对象。
(5)活跃线程。
注意点:开始进行标记前,需要先暂停应用线程。不然是没法统计的;统计完垃圾对象之后,回收器将会在接下来的阶段中清除它们;暂停时间的长短取决于存活对象的多少;实际上GC判断对象是否可达看的是强引用;根搜索算法中,要真正宣告一个对象死亡,至少要经历两次标记过程(对象还有逃脱死亡命运的最后一次机会)
1.如果对象在进行根搜索后发现没有与GC Roots相连接的引用链,那它会被第一次标记并且进行一次筛选。筛选的条件是此对象是否有必要执行 finalize()方法(可看作析构函数,类似于OC中的dealloc,Swift中的deinit)。当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行。
2.如果该对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue队列中,并在稍后由一条由虚拟机自动建立的、低优先级的Finalizer线程去执行finalize()方法。finalize()方法是对象逃脱死亡命运的最后一次机会(因为一个对象的finalize()方法最多只会被系统自动调用一次),稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中让该对象重新引用链上的任何一个对象建立关联即可。而如果对象这时还没有关联到任何链上的引用,那它就会被回收掉。
分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。缺点是效率问题和空间问题。可以存在大量不连续的内存碎片,导致程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
为了解决效率问题。将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。优点是每次都是对其中的一块进行内存回收,内存分配时就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。缺点是内存缩小了一半,代价太大。
现在的商业虚拟机都采用复制收集算法来回收新生代,有研究表明,新生代中的对象98%是朝生夕死的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中的一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。即如果另外一块Survivor空间没有足够的空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。
复制收集算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制收集算法。
根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
部分整理算法:
整理的顺序
不同算法中,堆遍历的次数,整理的顺序,对象的迁移方式都有所不同。而整理顺序又会影响到程序的局部性。主要有以下3种顺序:
所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。
当前商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并无新的方法,只是根据对象的存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同厂商,不同版本的虚拟机所提供的垃圾收集器可能有很大差别。下面以HotSpot JVM1.6的垃圾收集器为例
如果两个收集器之间存在连线,就说明他们可以搭配使用。并没有最好的收集器这一说,我们需要选择的是对具体应用最合适的收集器。
关键字:新生代、单线程
特点是它在进行垃圾收集时,必须暂停其他所有工作线程,直到他收集结束。体验贼差。
关键字:新生代、Serial收集器的多线程版本
关键字:新生代、复制算法、并行多线程、吞吐量优先(这里的吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间))
参数设置:
-XX:MaxGCPauseMillis 控制最大垃圾收集停顿时间。(大于0的毫秒数)停顿时间缩短是以牺牲吞吐量和新生代空间换取的。(新生代调的小,吞吐量跟着小,垃圾收集时间就短,停顿就小)。
-XX:GCTimeRatio 直接设置吞吐量大小,0<x<100 的整数,允许的最大GC时间=1/(1+x)。
-XX:+UseAdaptiveSizePolicy 一个开关参数,开启GC自适应调节策略(GC Ergonomics),将内存管理的调优任务(新生代大小-Xmn、Eden与Survivor区的比例-XX:SurvivorRatio、晋升老年代对象年龄-XX: PretenureSizeThreshold 、等细节参数)交给虚拟机完成。这是Parallel Scavenge收集器与ParNew收集器的一个重要区别,另一个是吞吐量。
关键字:老年代、单线程
关键字:老年代、多线程
关键字:以获取最短回收停顿时间为目标、并发收集。
目前很大一部分Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验,CMS收集器就非常符合这类应用的需求。
优点:并发收集,低停顿。(使用的“标记-清除”算法)
缺点:对CPU资源非常敏感;无法处理浮动垃圾;算法原因会产生空间碎片。
它是当前收集器技术发展的最前沿成果。与CMS相比有两个显著改进:
对象头组成:
1,Mark Word(Mark Word记录了对象和锁有关的信息)
2,指向类的指针(类结构信息保存在方法区。实例和对象则存在堆。这里指针指向方法区中类)
3,数组长度(只有数组对象才有)
实例数据
对齐填充字节
JVM要求java的对象占的内存大小应该是8bit的倍数,所以后面有几个字节用于把对象的大小补齐至8bit的倍数。
虚拟机将为对象分配内存,即把一块确定大小的内存从 Java 堆中划分出来。(对象所需内存的大小在类加载完成后便可完全确定)
内存分配 根据 Java堆内存是否绝对规整 分为两种方式:指针碰撞 & 空闲列表。(选择方式取决于Java堆内存是否规整)
Java堆内存规整:已使用的内存在一边,未使用内存在另一边。
Java堆内存不规整:已使用的内存和未使用内存相互交错。Java堆是否规整由所采用的垃圾收集器是否带有压缩整理功能决定。
使用带Compact过程的垃圾收集器时,采用指针碰撞。(如Serial、ParNew垃圾收集器)
使用基于Mark_sweep算法的垃圾收集器时,采用空闲列表。(如CMS垃圾收集器)
(1)假设Java堆内存绝对规整,内存分配将采用指针碰撞。
(2)分配形式:已使用内存在一边,未使用内存在另一边,中间放一个作为分界点的指示器
那么,分配对象内存 = 把指针向 未使用内存 移动一段 与对象大小相等的距离。
(1)假设Java堆内存不规整,内存分配将采用空闲列表。
(2)分配形式:虚拟机维护着一个记录可用内存块 的列表,在分配时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
线程不安全的问题(举个例子,分配内存时指针还没修改就被拿去用了)
解决办法
虚拟机采用 CAS + 失败重试的方式 保证更新操作的原子性。
即每个线程在Java堆中预先分配一小块内存(本地线程分配缓冲(Thread Local Allocation Buffer ,TLAB)),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时才需要同步锁。(虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。)
内存分配完成后,虚拟机需要将分配到的内存空间初始化为零(不包括对象头)
设置这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。
信息存放在对象的对象头。值得注意的是,从java虚拟机角度看,一个新的java对象创建完毕,但是从程序开发角度来说,还需要进行初始化。
最简单的访问也会涉及:Java堆、Java栈、方法区。
访问的对象是类型数据和对象实例数据,我举个例子就很容易理解了:
Object obj=new Object();
Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。
Hashtable实现了Mpa接口,他不属于Set或者List。还需要注意的是,虽然同步容器的所有方法都加了锁,但是对这些容器的复合操作无法保证其线程安全性。好比Vector方法的size()方法都加了锁,如果有一个方法体,先调用size(),再调用remove(),那就需要给这个方法体加上synchronized。
Vector可以设置增长因子,ArrayList不行。Vector是一种老的动态数组,是线程同步的,效率很低。(补充:除开Vector,实现了Map接口的Hashtable、HashMap都可以设置增长因子。)然后ArrayList增加长度是有计算公式的,和Vector不一样。{((旧容量 * 3) / 2) + 1}
//这个挺有意思的,HashSet虽然没有直接继承Map接口,但是他内部有一个HashMap对象。没错,就是靠HashMap来实现的。
/**
*往HashSet中添加元素
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
在HashMap中进行查找是否存在这个key,value始终是一样的:
1、 如果hash码值不相同,说明是一个新元素,存;
2、 如果hash码值相同,且equles判断相等,说明元素已经存在,不存;
3、 如果hash码值相同,且equles判断不相等,说明元素不存在,存;
HashSet是基于Hash算法实现的,其性能通常都优于TreeSet。为快速查找而设计的Set,我们通常都应该使用HashSet,在我们需要排序的功能时,我们才使用TreeSet。
TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset中的数据是自动排好序的,不允许放入null值。
HashSet 是哈希表实现的,HashSet中的数据是无序的。允许一个key == null。
HashMap 非线程安全,基于哈希表(散列表)实现。使用HashMap要求添加的键类明确定义了hashCode()和equals()[可以重写hashCode()和equals()],为了优化HashMap空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap的实现中采用的是链表法。
TreeMap:非线程安全基于红黑树实现,TreeMap没有调优选项,因为该树总处于平衡状态。
HashTable:线程安全基于哈希表。与HashMap很相似。
-HashMap和HashTable的区别:
- HashMap允许将null作为一个entry的key或者value,而Hashtable不允许。
- HashMap把Hashtable的contains方法去掉了,改成containsValue和 containsKey。因为contains方法容易让人引起误解。
- HashTable继承自Dictionary类,而HashMap是Java1.2引进的Map interface的一个实现。
- HashTable的方法是Synchronize的,而HashMap不是,在多个线程访问Hashtable时,不需要自己为它的方法实现同步,而HashMap就必须为之提供外同步。
- Hashtable和HashMap采用的hash/rehash算法都大概一样,所以性能不会有很大的差异。
//截取一段TreeMapEntry<K,V>代码
private static final boolean RED = false;
private static final boolean BLACK = true;
static final class TreeMapEntry<K,V> implements Map.Entry<K,V> {
K key;
V value;
TreeMapEntry<K,V> left;
TreeMapEntry<K,V> right;
TreeMapEntry<K,V> parent;
boolean color = BLACK;
//...
//省略其他代码
}
//截取一段HashMap.Node<K,V>代码
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
//...
//省略其他代码
}
泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。 Java语言引入泛型的好处是安全简单。
泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException。
泛型擦除
Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。
限定通配符
List<?> 是一个未知类型的List,而List<Object> 其实是任意类型的List。
可以把List<String>, List<Integer>赋值给List<?>,却不能把List<String>赋值给 List<Object>。
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
效率低的原因
Reflection和Introspection。
内省作用:在运行时检查一个对象的类型或者属性。
内省示例:instanceof 判断对象类型
反射作用:在运行时检查或者修改一个对象信息。
Class c = int[][].class;
Class<?> c = Class.forName("java.lang.String");
Class<Integer> integerWrapper = Integer.TYPE;
Class<Double> doubleWrapper = Double.TYPE;
Class<Void> voidWrapper = Void.TYPE;
//返回Field所在的类
java.lang.reflect.Field.getDeclaringClass()
//返回Method所在的类
java.lang.reflect.Method.getDeclaringClass()
//返回Constructor所在的类
java.lang.reflect.Constructor.getDeclaringClass()
//返回调用类的父类
Class.getSuperclass()
//返回调用类所有公共类、接口、枚举组成的 Class 数组,包括继承的
Class.getClasses()
//返回调用类显示声明的所有类、接口、枚举组成的 Class 数组
Class.getDeclaredClasses()
java.lang.reflect.Modifier
提供了对 Class 修饰符的解码,我们可以使用 Class.getModifiers() 获得调用类的修饰符的二进制值,然后使用 Modifier.toString(int modifiers) 将二进制值转换为字符串,Modifier.toString() 方法实现如下:
//基于sdk29 android.jar
public static String toString(int mod) {
StringBuilder sb = new StringBuilder();
int len;
if ((mod & PUBLIC) != 0) sb.append("public ");
if ((mod & PROTECTED) != 0) sb.append("protected ");
if ((mod & PRIVATE) != 0) sb.append("private ");
/* Canonical order */
if ((mod & ABSTRACT) != 0) sb.append("abstract ");
if ((mod & STATIC) != 0) sb.append("static ");
if ((mod & FINAL) != 0) sb.append("final ");
if ((mod & TRANSIENT) != 0) sb.append("transient ");
if ((mod & VOLATILE) != 0) sb.append("volatile ");
if ((mod & SYNCHRONIZED) != 0) sb.append("synchronized ");
if ((mod & NATIVE) != 0) sb.append("native ");
if ((mod & STRICT) != 0) sb.append("strictfp ");
if ((mod & INTERFACE) != 0) sb.append("interface ");
if ((len = sb.length()) > 0) /* trim trailing space */
return sb.toString().substring(0, len-1);
return "";
}
注意事项
Member是一个接口,代表 Class 的成员。
Member 有三个实现类:
成员变量。每个成员变量有类型和值。
java.lang.reflect.Field 提供了两个方法获去变量的类型:
try {
Class<Double> doubleWrapper = Double.TYPE;
Field[] fields = doubleWrapper.getFields();
for (Field field : fields) {
printFormat("Field:%s \n",field.getName());
printFormat("Type:\n %s\n",field.getType().getCanonicalName());
printFormat("GenericType:\n %s\n",field.getGenericType().toString());
printFormat("\n\n");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
继承的方法(包括重载、重写和隐藏的)会被编译器强制执行,这些方法都无法反射。
因此,反射一个类的方法时不考虑父类的方法,只考虑当前类的方法。由修饰符、返回值、参数、注解和抛出的异常组成。
Class<Double> doubleWrapper = Double.TYPE;
Method[] declaredMethods = doubleWrapper.getDeclaredMethods();
房产中介就是一个常见的代理。代理的概念里有几个关键词:静态代理、动态代理、代理模式下面我们只讨论动态代理。
来个简单的动态代理的例子
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 传入ClassLoader
new Class[] { Hello.class }, // 传入要实现的接口
handler); // 传入处理调用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
Java标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
动态代理是通过Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。
@Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
@Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
@SuppressWarnings - 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(或者说 元注解)是:
@Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
@Documented - 标记这些注解是否包含在用户文档中。
@Target - 标记这个注解应该是哪种 Java 成员。
@Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:
@SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
@FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
@Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
@interface 用来声明 Annotation,@Documented 用来表示该 Annotation 是否会出现在 javadoc 中, @Target 用来指定 Annotation 的类型,@Retention 用来指定 Annotation 的策略。
有一篇写的特别好的文章可以看一看:https://www.cnblogs.com/xdp-gacl/p/3622275.html
关键字:Thread、继承Thread、Runnable接口、Callable接口和FutureTask、线程池。
总结
线程创建练习
private void ThreadInit(){
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println("第1种方式:new Thread 1");
}
};
t1.start();
SystemClock.sleep(1);
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("第2种方式:new Thread 2");
}
});
t2.start();
SystemClock.sleep(1);
FutureTask<String> ft = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
String result = "第3种方式:new Thread 3";
return result;
}
});
Thread t3 = new Thread(ft);
t3.start();
// 线程执行完,才会执行get(),所以FutureTask也可以用于闭锁
String result = null;
try {
//这里会阻塞,直到线程返回值
result = ft.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
SystemClock.sleep(1);
ExecutorService pool = Executors.newFixedThreadPool(5);
Future<String> future = pool.submit(new Callable<String>(){
@Override
public String call() throws Exception {
String result = "第4种方式:new Thread 4";
return result;
}
});
pool.shutdown();
try {
//shutdown方法调用了,future.get()依然能拿到数据,但是使用shutdownNow()就不行了。
System.out.println(future.get());
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
新建、初始。
新创建了一个线程对象,但还没有调用start()方法。
就绪。
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
表示线程阻塞于锁。"阻塞状态"和"等待状态"的区别是"阻塞状态"在等待一个排他锁,这个事件将另一个线程放弃这个锁的时候发生;而"等待状态"则是等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
该状态不同于WAITING,它可以在指定的时间后自行返回。
表示该线程已经执行完毕。
简单来说wait()会释放对象锁资源而sleep()不会释放对象锁资源(wati是超类的方法,他才和锁机制密切相关)。但是 wait 和sleep 都会释放cpu资源;sleep是线程方法,wait是object方法;
一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
一种特殊的wait,当前运行线程调用另一个线程的join方法,当前线程进入阻塞状态直到另一个线程运行结束等待该线程终止。 注意该方法也需要捕捉异常。
等待调用join方法的线程结束,再继续执行。
当多个线程访问一个对象时,如果不考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。
以Vector为例,他里面的方法都加上了synchronized关键字,但是他是相对线程安全的。
例子:往某个Vector对象里存值,要求集合中没有重复的元素存在。
public static void put(int size,Vector vector) {
if (!vector.contains(size))
vector.add(size);
}
首先分析一波:生活中,代码是有先后执行顺序的,所以要模拟一种理想情况。不考虑线程在运行时环境下的调度和交替执行的情况下,假设有两个线程同时进入put()方法,传递的参数都是一样的,当线程1执行if (!vector.contains(element)) 后还没有执行vector.add(element); 时,线程2进来了,此时**vector.contains(element)**还是返回false,这样的结果会导致两个数据都加入到了vector。这就是不安全的。
解决办法也很简单
if (!vector.contains(element))
synchronized(this){
if (!vector.contains(element)) {
vector.add(element);
}
}
产生死锁的四个条件
定义:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。
JMM定义了8种原子操作
1.lock 锁定 : 把主内存中的一个变量标志为一个线程独享的状态
2.unlock 解锁 : 把主内存中的一个变量释放出来
3.read 读:将主内存中的变量读到工作内存中
4.load 加载:将工作内存中的变量加载到副本中
5.use 使用:当执行引擎需要使用到一个变量时,将工作内存中的变量的值传递给执行引擎
6.assign 赋值:将执行引擎收的的值赋值给工作内存中的变量
7.store 存储:将工作内存中的变量的值传到主内存中
8.write 写入:将store得到值放到主内存的变量中
对于上述原子操作有以下规定
通过加入内存屏障和禁止重排序来优化实现的。
硬件层内存屏障分为2种:Load Barrier 和 Store Barrier即读屏障和写屏障。
java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad
基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
oadLoad,StoreStore,LoadStore,StoreLoad实际上是Java对上面两种屏障的组合,来完成一系列的屏障和数据同步功能:
(1)线程写 volatile 变量的过程:先改变线程工作内存中 volatile 变量副本的值。再将改变后的副本的值从工作内存刷新的主内存。
(2)线程读 volatile 变量的过程:从主内存中读取 volatile 变量的最新值到线程的工作内存中。再从工作内存中读取 volatile 变量的副本。
JDK1.5之后,可以使用volatile变量禁止指令重排序。
处理器为了提高运行效率,在JVM中的及时编译存在指令重排序的优化,它会改变各个语句的执行顺序,但是不改变运行结果。指令重排序只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现问题。
内存屏障与禁止重排序有一定联系,编译时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不
具有原子性。
可以通过synchronized和Lock来实现更大范围操作的原子性。
JAVA线程间通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
内存模型抽象示意图如下:
如图所示,线程A和线程B之间要通信的话,必须要经历2个步骤。
(1)当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
(2)当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
持有同一个锁的两个同步块只能串行地进入。
由JMM原子操作规定中获得的。lock和unlock是成对的,可多次lock。线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
synchronized用的锁是存在Java对象头里的。
JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步是使用monitorenter和monitorexit指令实现的,monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行monitorenter指令时,首先要去尝试获取对象的锁,如果这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1;相应地,在执行monitorexit指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。如果获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。
由于Java的线程是映射到操作系统的原生线程之上的
,如果要阻塞或唤醒一条线程,都需要操作系统来帮忙完成,这就需要从用户态转换到核心态
中,因此状态转换需要耗费很多的处理器时间。所以synchronized是Java语言中的一个重量级操作。在JDK1.6中,虚拟机进行了一些优化,譬如在通知操作系统阻塞线程之前加入一段自旋等待过程,避免频繁地切入到核心态中:
synchronized与java.util.concurrent包中的ReentrantLock相比,由于JDK1.6中加入了针对锁的优化措施(见后面),使得synchronized与ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更丰富的功能,而不一定有更优的性能,所以在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步。
监视器锁(Monitor)本质是依赖于底层的操作系统的Mutex Lock(互斥锁)来实现的。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
互斥锁:用于保护临界区,确保同一时间只有一个线程访问数据。对共享资源的访问,先对互斥量进行加锁,如果互斥量已经上锁,调用线程会阻塞,直到互斥量被解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。
mutex的工作方式:
由上可知,synchronized 线程执行互斥代码过程可简述为:获取互斥锁;清空工作内存;从主内存中拷贝变量最新值到工作内存;执行代码;将更改后的共享变量刷新到主内存;释放互斥锁。
ThreadLocal提供了线程的局部变量,每个线程都可以通过ThreadLocalMap的set()、get()和remove()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。ThreadLocal可以类比浏览器,不同的浏览器对Cookie是隔离的(在IE会登录bilibili记住账号,换Chrome登录依然要输入账号密码)。
ThreadLocal的静态内部类ThreadLocalMap。Thread中也有用到。
他有一个Entry[] table类,里面存着以ThreadLocal<?> 为键,Object为值的变量。
public class Thread implements Runnable {
......(其他源码)
/*
* 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
*/
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,主要用于父子线程间ThreadLocal变量的传递。
* 该映射由InheritableThreadLocal类维护。
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
......(其他源码)
}
ThreadLocal可以让我们拥有当前线程的变量,对于新建对象与当前工作线程有关的操作比如,JDBC中频繁创建数据库连接池的操作,交由ThreadLocal来进行管理,可以保证事务不会混乱。当前线程的操作都是用同一个Connection。
public class DBUtil {
//数据库连接池
private static BasicDataSource source;
//为不同的线程管理连接
private static ThreadLocal<Connection> local;
static {
try {
//加载配置文件
Properties properties = new Properties();
//获取读取流
InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");
//从配置文件中读取数据
properties.load(stream);
//关闭流
stream.close();
//初始化连接池
source = new BasicDataSource();
//设置驱动
source.setDriverClassName(properties.getProperty("driver"));
//设置url
source.setUrl(properties.getProperty("url"));
//设置用户名
source.setUsername(properties.getProperty("user"));
//设置密码
source.setPassword(properties.getProperty("pwd"));
//设置初始连接数量
source.setInitialSize(Integer.parseInt(properties.getProperty("initsize")));
//设置最大的连接数量
source.setMaxActive(Integer.parseInt(properties.getProperty("maxactive")));
//设置最长的等待时间
source.setMaxWait(Integer.parseInt(properties.getProperty("maxwait")));
//设置最小空闲数
source.setMinIdle(Integer.parseInt(properties.getProperty("minidle")));
//初始化线程本地
local = new ThreadLocal<>();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
if(local.get()!=null){
return local.get();
}else{
//获取Connection对象
Connection connection = source.getConnection();
//把Connection放进ThreadLocal里面
local.set(connection);
//返回Connection对象
return connection;
}
}
//关闭数据库连接
public static void closeConnection() {
//从线程中拿到Connection对象
Connection connection = local.get();
try {
if (connection != null) {
//恢复连接为自动提交
connection.setAutoCommit(true);
//这里不是真的把连接关了,只是将该连接归还给连接池
connection.close();
//既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
local.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
ThreadLocal 可以让线程独占资源,存储于线程内部,避免线程堵塞造成 CPU 吞吐下降。在每个 Thread 中包含一个 ThreadLocalMap,ThreadLocalMap 的 key 是 ThreadLocal 的对象,value 是独享数据。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize, 即使有其他空闲线程能够执行新来的任务,也会继续创建线程;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;当阻塞队列是无界队列,则maximumPoolSize则不起作用,因为无法提交至核心线程池的线程会一直持续地放入workQueue。
非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长。
keepAliveTime的单位。可选单位在枚举类TimeUnit里。比如天、时、分、秒、毫秒、微妙、纳秒。
用来保存等待被执行的任务的阻塞队列. 在JDK中提供了如下阻塞队列:
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名。默认为DefaultThreadFactory。
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
分析:核心线程数和允许最大线程数一致,有关超时的设置无效了(除非ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true),线程池的线程数量达corePoolSize后,不会释放线程。而且使用无界队列,可以一直往里添加任务。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
分析:初始化的线程池中只有一个线程,如果该线程异常结束,会重新创建一个新的线程继续执行任务,唯一的线程可以保证所提交任务的顺序执行.使用了无界队列,所以SingleThreadPool永远不会拒绝, 即饱和策略失效。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
分析:线程池的线程数可达到Integer.MAX_VALUE,即2147483647,内部使用SynchronousQueue作为阻塞队列;newCachedThreadPool在没有任务执行时,当线程的空闲时间超过keepAliveTime,会自动释放线程资源;当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销;
特点如下
execute和submit。
execute 只能接受Runnable类型的任务。
void execute(Runnable command);
submit不管是Runnable还是Callable类型的任务都可以接受,但是Runnable返回值均为void,所以使用Future的get()获得的还是null。
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
如果是不需要关注返回值的场景,使用execute
工作中用的比较多submit方法,可以通过Future的get()方法得到线程的运行结果
shutdown()和shutdownNow()
void shutdown();
List<Runnable> shutdownNow();
方法解析:
shutdown()这种方法是有序的进行停止,在此之前提交的任务都可以继续执行,而执行此方法后如果继续往线程池丢任务,则不会再去执行任务。调用这个方法,线程池不会等待(wait)在执行的任务执行完成,可以使用awaitTermination实现这个目的。他会依次中断那些没有中断,并且是空闲的线程。
shutdownNow()这种方法是停止所有线程(正在执行的和等待的),并返回任务列表(等待执行的任务),已经执行的任务是不会返回的。
部分参考文章及参考书籍,侵删。
《深入理解Java虚拟机》
《Java语言规范》
《Java并发编程的艺术》
双亲委派模型与线程上下文类加载器
4种垃圾收集算法及7种垃圾收集器
synchronized原理总结
volatile内存可见性和指令重排
ThreadLocal就是这么简单
深入理解 Java 反射:Class (反射的入口)
文章浏览阅读258次。多线程基础之设计模式Future模式_线程 future
文章浏览阅读1w次,点赞17次,收藏60次。本文档仅对ccs编程过程中所出现的error#5、error#10008-D、error#10010做简要讲解在使用ccs对dsp编程过程中,用户可能会参考一些例程或在维护优化时阅读他人程序,而在导入程序时会出现各种各样的错误或警告,下面对编者在修改程序时遇到的error#5、error#10008-D和error#10010做简要讲解。1.error#5的错误更正讲解在ccs中导入其..._cannot find file "libc.a
文章浏览阅读359次。题意:给定m(m思路:暴力找出第一个串的所有长度大于等于3的子串,用KMP算法求其是否为剩下m-1个串的子串。为了复用next数组,枚举子串时先固定起点(求一遍next数组即可),然后由长到短枚举子串(剪枝)。#include #include using namespace std;#define N 60char s[12][N+5],t[N+5],res[N+5];int
文章浏览阅读1.4k次。reshape把指定的矩阵改变形状,但是元素个数不变,例如,行向量:a = [1 2 3 4 5 6]执行下面语句把它变成3行2列:b = reshape(a,3,2)执行结果:b =1 42 53 6若a=[1 2 34 5 67 8 9]使用reshpe后想得到b=[1 2 3 4 5 6 7 8 9]只需要将a转置一下就可以了:b=reshape(a',1,9)---------------..._matlab中reshape的含义
文章浏览阅读1k次。运算符在数学和C语言中的区别刚开始学C语言的人,一般都认为C语言中的运算符跟数学中的运算符完全相同,没必要去考虑和研究,从而在利用过程中经常出错而把学习C语言越来越难或神秘化,其实学C语言并不是很难的事,要把握有些重要技巧,很容易学会.著名计算机科学家沃思(Nikiklaus Wirth)说“程序=算法+数据类型”,要好好学会程序,首先要深入了解算法,而了解算法事实上指的是就是正确地了解和利用运算..._c语言中的加减乘除和数学中的加减乘除有什么不同【
文章浏览阅读3.9k次。一、三大框架基本结构1.为什么需要框架说明: 如果生产环境下的项目,都是从头(从底层写起)开发,难度太大了,并且开发的效率极其低下. 所以为了让项目快速的上线部署. 将某些特定的功能.进行了高级的封装. 那么我们如果需要使用封装后的API.,则必须按照人家的要求编码2.框架的分类:1.Spring框架:整个框架中负责“宏观调控”的(主导),负责整合其它的第三方的框架2.SpringMVC框架:主要负责实现前后端数据的交互3.Mybatis框架/MybatisPlus框架:持久层框.._后端框架三大框架
文章浏览阅读2次。 堆栈原理: 数组模拟堆栈: //数组模拟栈class ArrayStack{ //栈顶 private int top = -1; private int maxSize; private int[] arrayStack; public ArrayStack(int maxSize){ this.maxSi...
文章浏览阅读742次,点赞16次,收藏17次。不选: Enforce portability rules to share this project with others。勾选: Configure Advanced Settings after project creation。保存类型(T):Understand projects (*.udb)勾选:Include subdirectories (包含子文件夹)Additional Filters: (空)单击 文件夹 lab1。文件名(N):lab1。双击 文件夹 boot。_understand 6.5.1176
文章浏览阅读969次。在从零开始带你成为MySQL实战优化高手学习笔记(一)中学习到一条语句到底是怎么执行的,从链接获取数据到通过查询解析器解析SQL语句表达的什么意思,解析之后由查询优化器生成查询路径树,选出一条最优查询路径调用存储引擎接口..._mysql_global_status_innodb_buffer_pool_reads
文章浏览阅读8.8k次,点赞6次,收藏12次。传统的表单控件十分简陋,可以说是很难看,那怎么办?方法:我们自己做一个好看的样式出来,用各种标签啊,css啊,是可以做到的。如图:做出这样一个样子应该是很简单的,但是怎么让他具有上传的功能的呢?那就使用代理的方法,点击上传就等于点击(上传文件表单控件)废话不多说,直接上代码:html:测试插件body{font_文件上传框很丑
文章浏览阅读4.8k次,点赞3次,收藏18次。js简单表格操作,对表格进行增删改,效果图:全部代码:<!DOCTYPE html><html><head> <meta charset="utf-8" /> <script type="text/javascript" src="js/jquery.2.1.4.min.js" ></sc_"var str = '序号名字
文章浏览阅读1.1w次,点赞8次,收藏99次。今天通过一份销售数据,聊聊Power BI数据分析。一、分析数据数据源总的有四个表,店铺资料,销售目标,销售数据_本期,销售数据_去年同期。各表表头如下:1店铺资料表:2销售目标:3销售数据_本期:4销售数据_去年同期:数据中包含多个城市、督导、店铺的数据,我希望经过分析后能得到各个城市/店铺的销售情况,即业绩、业绩完成率、业绩贡献度、业绩增长率、各销售人员的销售能力等。此次..._powerbi汇总销售人员业绩包括无销售记录的人