java面试-有这一篇就够了(1-java基础部分)
前言
所谓金三银四,程序员的春天大概就是这一两个月,三四月一般是涨工资的季节.
马云曾经说过,离职无非两个原因
1,过的开心
2,钱没到位
苦逼的程序员经过各种加班,大多数是因为钱没到位导致辞职的,可能还有小部分是其他原因
于是在我这离职的一两个月中,精心整理了面试宝典,分为很多部分,因为我会不定期更新,一是为了巩固自己的知识,二是把自己的一些面试心得分享给大家,同为程序员小团体,希望大家不要在面试的时候被坑....同时也希望大家能够通过我整理的资料对自己的技术不足之处尽快弥补,因为很可能一个面试官很细节的提问,同时面试的人没答出来你答出来了就被录取了,当然掌握更多的知识点对自己来说肯定也不是什么坏处,毕竟平常写代码不会太关注与理论知识,而面试恰恰有一部分是关于理论知识的.所以掌握理论基础知识还是非常重要的.
文章可能会非常长,可能会导致呕吐,建议尽早离开案发市场
1,抽象类(abstract class)和接口(interface)有什么异同?
不同:
抽象类:
1.抽象类中可以定义构造器
2.可以有抽象方法和具体方法
3.接口中的成员全都是 public 的
4.抽象类中可以定义成员变量
5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法
6.抽象类中可以包含静态方法
7.一个类只能继承一个抽象类
接口:
1.接口中不能定义构造器
2.方法全部都是抽象方法
3.抽象类中的成员可以是 private、默认、protected、public
4.接口中定义的成员变量实际上都是常量
5.接口中不能有静态方法
6.一个类可以实现多个接口
相同:
1.不能够实例化
2.可以将抽象类和接口类型作为引用类型
3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要
被声明为抽象类
2,String s = “Hello”;s = s + ” world!”;这两行代码执行后,原始的 String 对象中的内容到底变了没有?
没有。因为 String 被设计成不可变(immutable)类,所以它的所有对象都是不可变对象。在这段代码中,s 原先指
向一个 String 对象,内容是 “Hello”,然后我们对 s 进行了“+”操作,那么 s 所指向的那个对象是否发生了改变呢?
答案是没有。这时,s 不指向原来那个对象了,而指向了另一个 String 对象,内容为”Hello world!”,原来那个对象还存在于内存之中,只是 s 这个引用变量不再指向它了。
通过上面的说明,我们很容易导出另一个结论,如果经常对字符串进行各种各样的修改,或者说,不可预见的修
改,那么使用 String 来代表字符串的话会引起很大的内存开销。因为 String 对象建立之后不能再改变,所以对于每
一个不同的字符串,都需要一个 String 对象来表示。这时,应该考虑使用 StringBuffer 类,它允许修改,而不是每个
不同的字符串都要生成一个新的对象。并且,这两种类的对象转换十分容易。同时,我们还可以知道,如果要使用内容
相同的字符串,不必每次都 new 一个 String。例如我们要在构造器中对一个名叫 s 的 String 引用变量进行初始化,
把它设置为初始值,应当这样做:
public class Demo {
private String s;
...
s = "Initial Value";
...
}
而非
s = new String("Initial Value");
后者每次都会调用构造器,生成新对象,性能低下且内存开销大,并且没有意义,因为 String 对象不可改变,所
以对于内容相同的字符串,只要一个 String 对象来表示就可以了。也就说,多次调用上面的构造器创建多个对象,他
们的 String 类型属性 s 都指向同一个对象。
上面的结论还基于这样一个事实:对于字符串常量,如果内容相同,Java 认为它们代表同一个 String 对象。而
用关键字 new 调用构造器,总是会创建一个新的对象,无论内容是否相同。 至于为什么要把 String 类设计成不可变
类,是它的用途决定的。其实不只 String,很多 Java 标准类库中的类都是不可变的。在开发一个系统的时候,我们
有时候也需要设计不可变类,来传递一组相关的值,这也是面向对象思想的体现。不可变类有一些优点,比如因为它
的对象是只读的,所以多线程并发访问也不会有任何问题。当然也有一些缺点,比如每个不同的状态都要一个对象来
代表,可能会造成性能上的问题。所以 Java 标准类库还提供了一个可变版本,即 StringBuffer。
3,调用下面的方法,得到的返回值是什么?
public int getNum(){
try {
int a = 1/0;
return 1;
} catch (Exception e) {
return 2;
}finally{
return 3;
}
代码在走到第 3 行的时候遇到了一个 MathException,这时第四行的代码就不会执行了,代码直接跳转到 catch
语句中,走到第 6 行的时候,异常机制有这么一个原则如果在 catch 中遇到了 return 或者异常等能使该函数终止的话
那么有 finally 就必须先执行完 finally 代码块里面的代码然后再返回值。因此代码又跳到第 8 行,可惜第 8 行是一个
return 语句,那么这个时候方法就结束了,因此第 6 行的返回结果就无法被真正返回。如果 finally 仅仅是处理了一个
释放资源的操作,那么该道题最终返回的结果就是 2。因此上面返回值是 3。
4,ArrayList 内部用什么实现的?
(回答这样的问题,不要只回答个皮毛,可以再介绍一下 ArrayList 内部是如何实现数组的增加和删除的,因为数
组在创建的时候长度是固定的,那么就有个问题我们往 ArrayList 中不断的添加对象,它是如何管理这些数组呢?)
ArrayList 内部是用 Object[]实现的。接下来我们分别分析 ArrayList 的构造、add、remove、clear 方法的实现
原理。
一、构造函数
1)空参构造
/**
* Constructs a new {@code ArrayList} instance with zero initial capacity.
*/
public ArrayList() {
array = EmptyArray.OBJECT;
}
array 是一个 Object[]类型。当我们 new 一个空参构造时系统调用了 EmptyArray.OBJECT 属性,EmptyArray 仅
仅是一个系统的类库,该类源码如下:
public final class EmptyArray {
private EmptyArray() {
}
public static final Boolean[] BOOLEAN = new Boolean[0];
public static final byte[] bYTE = new byte[0];
public static final char[] cHAR = new char[0];
public static final double[] dOUBLE = new double[0];
public static final int[] iNT = new int[0];
public static final Class<?>[] CLASS = new Class[0];
public static final Object[] OBJECT = new Object[0];
public static final String[] STRING = new String[0];
public static final Throwable[] THROWABLE = new Throwable[0];
public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}
也就是说当我们 new 一个空参 ArrayList 的时候,系统内部使用了一个 new Object[0]数组。
2)带参构造 1
/**
* Constructs a new instance of {@code ArrayList} with the specified
* initial capacity.
*
* @param capacity
* the initial capacity of this {@code ArrayList}.
*/
public ArrayList(int capacity) {
if (capacity < 0) {
throw new IllegalArgumentException("capacity < 0: " + capacity);
}
array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}
该构造函数传入一个 int 值,该值作为数组的长度值。如果该值小于 0,则抛出一个运行时异常。如果等于 0,则
使用一个空数组,如果大于 0,则创建一个长度为该值的新数组。
3)带参构造 2
/**
* Constructs a new instance of {@code ArrayList} containing the elements of
* the specified collection.
*
* @param collection
* the collection of elements to add.
*/
public ArrayList(Collection<? extends E> collection) {
if (collection == null) {
throw new NullPointerException("collection == null");
}
Object[] a = collection.toArray();
if (a.getClass() != Object[].class) {
Object[] newArray = new Object[a.length];
System.arraycopy(a, 0, newArray, 0, a.length);
a = newArray;
}
array = a;
size = a.length;
}
如果调用构造函数的时候传入了一个 Collection 的子类,那么先判断该集合是否为 null,为 null 则抛出空指针异
常。如果不是则将该集合转换为数组 a,然后将该数组赋值为成员变量 array,将该数组的长度作为成员变量 size。这
里面它先判断 a.getClass 是否等于 Object[].class,其实一般都是相等的,我也暂时没想明白为什么多加了这个判断,
toArray 方法是 Collection 接口定义的,因此其所有的子类都有这样的方法,list 集合的 toArray 和 Set 集合的 toArray
返回的都是 Object[]数组。
这里讲些题外话,其实在看 Java 源码的时候,作者的很多意图都很费人心思,我能知道他的目标是啥,但是不知
道他为何这样写。比如对于 ArrayList, array 是他的成员变量,但是每次在方法中使用该成员变量的时候作者都会重
新在方法中开辟一个局部变量,然后给局部变量赋值为 array,然后再使用,有人可能说这是为了防止并发修改 array,
毕竟 array 是成员变量,大家都可以使用因此需要将 array 变为局部变量,然后再使用,这样的说法并不是都成立的,
也许有时候就是老外们写代码的一个习惯而已。
二、add 方法
add 方法有两个重载,这里只研究最简单的那个。
/**
* Adds the specified object at the end of this {@code ArrayList}.
*
* @param object
* the object to add.
* @return always true
*/
@Override
public Boolean add(E object) {
Object[] a = array;
int s = size;
if (s == a.length) {
Object[] newArray = new Object[s +
(s < (MIN_CAPACITY_INCREMENT / 2) ?
MIN_CAPACITY_INCREMENT : s >> 1)];
System.arraycopy(a, 0, newArray, 0, s);
array = a = newArray;
}
a[s] = object;
size = s + 1;
modCount++;
return true;
}
1、首先将成员变量 array 赋值给局部变量 a,将成员变量 size 赋值给局部变量 s。
2、判断集合的长度 s 是否等于数组的长度(如果集合的长度已经等于数组的长度了,说明数组已经满了,该重新
分配新数组了),重新分配数组的时候需要计算新分配内存的空间大小,如果当前的长度小于
MIN_CAPACITY_INCREMENT/2(这个常量值是 12,除以 2 就是 6,也就是如果当前集合长度小于 6)则分配 12 个
长度,如果集合长度大于 6 则分配当前长度 s 的一半长度。这里面用到了三元运算符和位运算,s >> 1,意思就是将
s 往右移 1 位,相当于 s=s/2,只不过位运算是效率最高的运算。
3、将新添加的 object 对象作为数组的 a[s]个元素。
4、修改集合长度 size 为 s+1
5、modCotun++,该变量是父类中声明的,用于记录集合修改的次数,记录集合修改的次数是为了防止在用迭代
器迭代集合时避免并发修改异常,或者说用于判断是否出现并发修改异常的。
6、return true,这个返回值意义不大,因为一直返回 true,除非报了一个运行时异常。
三、remove 方法
remove 方法有两个重载,我们只研究 remove(int index)方法。
/**
* Removes the object at the specified location from this list.
*
* @param index
* the index of the object to remove.
* @return the removed object.
* @throws IndexOutOfBoundsException
* when {@code location < 0 || location >= size()}
*/
@Override
public E remove(int index) {
Object[] a = array;
int s = size;
if (index >= s) {
throwIndexOutOfBoundsException(index, s);
}
@SuppressWarnings("unchecked")
E result = (E) a[index];
System.arraycopy(a, index + 1, a, index, --s - index);
a[s] = null;
// Prevent memory leak
size = s;
modCount++;
return result;
}
1、先将成员变量 array 和 size 赋值给局部变量 a 和 s。
2、判断形参 index 是否大于等于集合的长度,如果成了则抛出运行时异常
3、获取数组中脚标为 index 的对象 result,该对象作为方法的返回值
4、调用 System 的 arraycopy 函数,拷贝原理如下图所示。
5、接下来就是很重要的一个工作,因为删除了一个元素,而且集合整体向前移动了一位,因此需要将集合最后一
个元素设置为 null,否则就可能内存泄露。
6、重新给成员变量 array 和 size 赋值
7、记录修改次数
8、返回删除的元素(让用户再看最后一眼)
四、clear 方法
/**
* Removes all elements from this {@code ArrayList}, leaving it empty.
*
* @see #isEmpty
* @see #size
*/
@Override
public void clear() {
if (size != 0) {
Arrays.fill(array, 0, size, null);
size = 0;
modCount++;
}
}
如果集合长度不等于 0,则将所有数组的值都设置为 null,然后将成员变量 size 设置为 0 即可,最后让修改记录
加 1。
5,并发集合和普通集合如何区别?
并发集合常见的有 ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque 等。并发集合
位 于 java.util.concurrent 包 下 , 是 jdk1.5 之 后 才 有 的 , 主 要 作 者 是 Doug Lea
(http://baike.baidu.com/view/3141057.htm)完成的。
在 java 中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全
性和并发的可靠性。线程安全集合仅仅是给集合添加了 synchronized 同步锁,严重牺牲了性能,而且对并发的效率就
更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率。
参考阅读:
ConcurrentHashMap 是线程安全的 HashMap 的实现,默认构造同样有 initialCapacity 和 loadFactor 属性,
不过还多了一个 concurrencyLevel 属性,三属性默认值分别为 16、0.75 及 16。其内部使用锁分段技术,维持这锁
Segment 的数组,在 Segment 数组中又存放着 Entity[]数组,内部 hash 算法将数据较均匀分布在不同锁中。
put 操作:并没有在此方法上加上 synchronized,首先对 key.hashcode 进行 hash 操作,得到 key 的 hash 值。
hash 操作的算法和 map 也不同,根据此 hash 值计算并获取其对应的数组中的 Segment 对象(继承自 ReentrantLock),
接着调用此 Segment 对象的 put 方法来完成当前操作。
ConcurrentHashMap 基于 concurrencyLevel 划分出了多个 Segment 来对 key-value 进行存储,从而避免每
次 put 操作都得锁住整个数组。在默认的情况下,最佳情况下可允许 16 个线程并发无阻塞的操作集合对象,尽可能地
减少并发时的阻塞现象。
get(key)
首先对 key.hashCode 进行 hash 操作,基于其值找到对应的 Segment 对象,调用其 get 方法完成当前操
作。而 Segment 的 get 操作首先通过 hash 值和对象数组大小减 1 的值进行按位与操作来获取数组上对应位置的
HashEntry。在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的 HashEntry 产生不一致性,那
么 ConcurrentHashMap 是如何保证的?
对象数组大小的改变只有在 put 操作时有可能发生,由于 HashEntry 对象数组对应的变量是 volatile 类型
的,因此可以保证如 HashEntry 对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了 HashEntry 对象后,怎么能保证它及其 next 属性构成的链表上的对象不会改变呢?这点
ConcurrentHashMap 采用了一个简单的方式,即 HashEntry 对象中的 hash、key、next 属性都是 final 的,这也就
意味着没办法插入一个 HashEntry 对象到基于 next 属性构成的链表中间或末尾。这样就可以保证当获取到 HashEntry
对象后,其基于 next 属性构建的链表是不会发生变化的。
ConcurrentHashMap 默认情况下采用将数据分为 16 个段进行存储,并且 16 个段分别持有各自不同的锁
Segment,锁仅用于 put 和 remove 等改变集合对象的操作,基于 volatile 及 HashEntry 链表的不变性实现了读取
的不加锁。这些方式使得 ConcurrentHashMap 能够保持极好的并发支持,尤其是对于读远比插入和删除频繁的 Map
而言,而它采用的这些方法也可谓是对于 Java 内存模型、并发机制深刻掌握的体现。
推荐博客地址:http://m.oschina.net/blog/269037
7,List 的三个子类的特点
ArrayList 底层结构是数组,底层查询快,增删慢。
LinkedList 底层结构是链表型的,增删快,查询慢。
voctor 底层结构是数组 线程安全的,增删慢,查询慢。
8,List 和 Map、Set 的区别
1 结构特点
List 和 Set 是存储单列数据的集合,Map 是存储键和值这样的双列数据的集合;List 中存储的数据是有顺序,并且允许重复;Map 中存储的数据是没有顺序的,其键是不能重复的,它的值是可以有重复的,Set 中存储的数据是无序的,且不允许有重复,但元素在集合中的位置由元素的 hashcode 决定,位置是固定的(Set 集合根据 hashcode 来
进行数据的存储,所以位置是固定的,但是位置不是用户可以控制的,所以对于用户来说 set 中的元素还是无序的);
实现类
List 接口有三个实现类(LinkedList:基于链表实现,链表内存是散乱的,每一个元素存储本身内存地址的同时还存储下一个元素的地址。链表增删快,查找慢;ArrayList:基于数组实现,非线程安全的,效率高,便于索引,但不便于插入删除;Vector:基于数组实现,线程安全的,效率低)。
Map 接口有三个实现类(HashMap:基于 hash 表的 Map 接口实现,非线程安全,高效,支持 null 值和 null键;HashTable:线程安全,低效,不支持 null 值和 null 键;LinkedHashMap:是 HashMap 的一个子类,保存了记录的插入顺序;SortMap 接口:TreeMap,能够把它保存的记录根据键排序,默认是键值的升序排序)。
Set 接口有两个实现类(HashSet:底层是由 HashMap 实现,不允许集合中有重复的值,使用该方式时需要重写 equals()和 hashCode()方法;LinkedHashSet:继承与 HashSet,同时又基于 LinkedHashMap 来进行实现,底层使用的是 LinkedHashMp)。
区别
List 集合中对象按照索引位置排序,可以有重复对象,允许按照对象在集合中的索引位置检索对象,例如通过
list.get(i)方法来获取集合中的元素;Map 中的每一个元素包含一个键和一个值,成对出现,键对象不可以重复,值对
象可以重复;Set 集合中的对象不按照特定的方式排序,并且没有重复对象,但它的实现类能对集合中的对象按照特定
的方式排序,例如 TreeSet 类,可以按照默认顺序,也可以通过实现 Java.util.Comparator接口来自定义排序
方式。
HashMap 和 HashTable 有什么区别?
HashMap 是线程不安全的,HashMap 是一个接口,是 Map 的一个子接口,是将键映射到值得对象,不允许键值重复,
允许空键和空值;由于非线程安全,HashMap 的效率要较 HashTable 的效率高一些.
HashTable 是线程安全的一个集合,不允许 null 值作为一个 key 值或者 Value 值;
HashTable 是 sychronize,多个线程访问时不需要自己为它的方法实现同步,而 HashMap 在被多个线程访问的时
候需要自己为它的方法实现同步;
数组和链表的区别
数组是将元素在内存中连续存储的;它的优点:因为数据是连续存储的,内存地址连续,所以在查找数据的时候效
率比较高;它的缺点:在存储之前,我们需要申请一块连续的内存空间,并且在编译的时候就必须确定好它的空间的大
小。在运行的时候空间的大小是无法随着你的需要进行增加和减少而改变的,当数据两比较大的时候,有可能会出现
越界的情况,数据比较小的时候,又有可能会浪费掉内存空间。在改变数据个数时,增加、插入、删除数据效率比较低
链表是动态申请内存空间,不需要像数组需要提前申请好内存的大小,链表只需在用的时候申请就可以,根据需
要来动态申请或者删除内存空间,对于数据增加和删除以及插入比数组灵活。还有就是链表中数据在内存中可以在任
意的位置,通过应用来关联数据(就是通过存在元素的指针来联系)
链表和数组使用场景
数组应用场景:数据比较少;经常做的运算是按序号访问数据元素;数组更容易实现,任何高级语言都支持;构建
的线性表较稳定。
链表应用场景:对线性表的长度或者规模难以估计;频繁做插入删除操作;构建动态性比较强的线性表。
参考博客:http://blog.csdn.net/u011277123/article/details/53908387
要对集合更新操作时,ArrayList 和 LinkedList 哪个更适合?
1.ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构。
2.如果集合数据是对于集合随机访问 get 和 set,ArrayList 绝对优于 LinkedList,因为 LinkedList 要移动指针。
3.如果集合数据是对于集合新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数
据。
ArrayList 和 LinkedList 是两个集合类,用于存储一系列的对象引用(references)。例如我们可以用 ArrayList 来
存储一系列的 String 或者 Integer。那 么 ArrayList 和 LinkedList 在性能上有什么差别呢?什么时候应该用 ArrayList
什么时候又该用 LinkedList 呢?
一.时间复杂度
首先一点关键的是,ArrayList 的内部实现是基于基础的对象数组的,因此,它使用 get 方法访问列表中的任意一
个元素时(random access),它的速度要比 LinkedList 快。LinkedList 中的 get 方法是按照顺序从列表的一端开始检
查,直到另外一端。对 LinkedList 而言,访问列表中的某个指定元素没有更快的方法了。
假设我们有一个很大的列表,它里面的元素已经排好序了,这个列表可能是 ArrayList 类型的也可能是 LinkedList
类型的,现在我们对这个列表来进行二分查找(binary search),比较列表是 ArrayList 和 LinkedList 时的查询速度,
看下面的程序:
public class TestList{
public static final int N=50000; //50000 个数
public static List values; //要查找的集合
//放入 50000 个数给 value;
static{
Integer vals[]=new Integer[N];
Random r=new Random();
for(int i=0,currval=0;i<N;i++)...{
vals=new Integer(currval);
currval+=r.nextInt(100)+1;
}
values=Arrays.asList(vals);
}
//通过二分查找法查找
static long timeList(List lst){
long start=System.currentTimeMillis();
for(int i=0;i<N;i++)...{
int index=Collections.binarySearch(lst, values.get(i));
if(index!=i)
System.out.println("***错误***");
}
return System.currentTimeMillis()-start;
}
public static void main(String args[])...{
System.out.println("ArrayList 消耗时间:"+timeList(new ArrayList(values)));
System.out.println("LinkedList 消耗时间:"+timeList(new LinkedList(values)));
}
}
得到的输出是:
1. ArrayList 消耗时间:15
2. LinkedList 消耗时间:2596
这个结果不是固定的,但是基本上 ArrayList 的时间要明显小于 LinkedList 的时间。因此在这种情况下不宜用
LinkedList。二分查找法使用的随机访问(random access)策略,而 LinkedList 是不支持快速的随机访问的。对一个LinkedList 做随机访问所消耗的时间与这个 list 的大小是成比例的。而相应的,在 ArrayList 中进行随机访问所消耗的
时间是固定的。
这是否表明 ArrayList 总是比 LinkedList 性能要好呢?这并不一定,在某些情况下 LinkedList 的表现要优于
ArrayList,有些算法在 LinkedList 中实现 时效率更高。比方说,利用 Collections.reverse 方法对列表进行反转时,
其性能就要好些。看这样一个例子,加入我们有一个列表,要对其进行大量的插入和删除操作,在这种情况下 LinkedList
就是一个较好的选择。请看如下一个极端的例子,我们重复的在一个列表的开端插入一个元素:
1.import java.util.*;
2.public class ListDemo {
3. static final int N=50000;
4. static long timeList(List list){
5. long start=System.currentTimeMillis();
6. Object o = new Object();
7. for (int i=0;i<N;i++)
8. list.add(0, o);
9. return System.currentTimeMillis()-start;
10.
}
11. public static void main(String[] args) {
12. System.out.println("ArrayList 耗时:"+timeList(new ArrayList()));
13. System.out.println("LinkedList 耗时:"+timeList(new LinkedList()));
14.
}
15.
}
这时我的输出结果是
1. ArrayList 耗时:2463
2. LinkedList 耗时:15
二.空间复杂度
在 LinkedList 中有一个私有的内部类,定义如下:
1.private static class Entry {
2. Object element;
3. Entry next;
4. Entry previous;
5. }
每个 Entry 对象 reference 列表 中的一个元素,同时还有在 LinkedList 中它的上一个元素和下一个元素。一个
有 1000 个元素的 LinkedList 对象将有 1000 个链接在一起 的 Entry 对象,每个对象都对应于列表中的一个元素。
这样的话,在一个 LinkedList 结构中将有一个很大的空间开销,因为它要存储这 1000 个 Entity 对象的相关信息。
ArrayList 使用一个内置的数组来存 储元素,这个数组的起始容量是 10.当数组需要增长时,新的容量按如下公
式获得:新容量=(旧容量*3)/2+1,也就是说每一次容量大概会增长 50%。 这就意味着,如果你有一个包含大量元
素的 ArrayList 对象,那么最终将有很大的空间会被浪费掉,这个浪费是由 ArrayList 的工作方式本身造成 的。如果
没有足够的空间来存放新的元素,数组将不得不被重新进行分配以便能够增加新的元素。对数组进行重新分配,将会
导致性能急剧下降。如果我们知道一个 ArrayList 将会有多少个元素,我们可以通过构造方法来指定容量。我们还可
以通过 trimToSize 方法在 ArrayList 分配完毕之后去掉浪 费掉的空间。
三.总结
ArrayList 和 LinkedList 在性能上各有优缺点,都有各自所适用的地方,总的说来可以描述如下:
1.对 ArrayList 和 LinkedList 而言,在列表末尾增加一个元素所花的开销都是固定的。对 ArrayList 而言,主
要是在内部数组中增加一项,指向所添加的元素,偶 尔可能会导致对数组重新进行分配;而对 LinkedList 而言,这
个开销是统一的,分配一个内部 Entry 对象。
2.在 ArrayList 的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在 LinkedList 的中间
插入或删除一个元素的开销是固定的。
3.LinkedList 不支持高效的随机元素访问。
4.ArrayList 的空间浪费主要体现在在 list 列表的结尾预留一定的容量空间,而 LinkedList 的空间花费则体现在
它的每一个元素都需要消耗相当的空间
可以这样说:当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList 会提供比较好的性能;当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元
素时,就应该使用 LinkedList 了。
请用两个队列模拟堆栈结构
两个队列模拟一个堆栈,队列是先进先出,而堆栈是先进后出。模拟如下
队列 a 和 b
(1)入栈:a 队列为空,b 为空。例:则将”a,b,c,d,e”需要入栈的元素先放 a 中,a 进栈为”a,b,c,d,e”
(2)出栈:a 队列目前的元素为”a,b,c,,d,e”。将 a 队列依次加入 Arraylist 集合 a 中。以倒序的方法,将 a 中的集
合取出,放入 b 队列中,再将 b 队列出列。代码如下:
1. public static void main(String[] args) {
2. Queue<String> queue = new LinkedList<String>(); //a 队列
3. Queue<String> queue2=new LinkedList<String>(); //b 队列
4. ArrayList<String> a=new ArrayList<String>(); //arrylist 集合是中间参数
5. //往 a 队列添加元素
6. queue.offer("a");
7. queue.offer("b");
8. queue.offer("c");
9. queue.offer("d");
10. queue.offer("e");
11. System.out.print("进栈:");
12. //a 队列依次加入 list 集合之中
13. for(String q : queue){
14. a.add(q);
15. System.out.print(q);
16. }
17. //以倒序的方法取出(a 队列依次加入 list 集合)之中的值,加入 b 对列
18. for(int i=a.size()-1;i>=0;i--){
19. queue2.offer(a.get(i));
20. }
21. //打印出栈队列
22. System.out.println("");
23. System.out.print("出栈:");
24. for(String q : queue2){
25. System.out.print(q);
26. }
27. }
打印结果为(遵循栈模式先进后出):
进栈:a b c d e
出栈:e d c b a
垃圾回收几种方式
a. 标记回收法:遍历对象图并且记录可到达的对象,以便删除不可到达的对象,一般使用单线程工作并且可能产生内存碎片
b. 标记-压缩回收法:前期与第一种方法相同,只是多了一步,将所有的存活对象压缩到内存的一端,这样内存碎片就可以合成一大块可再利用的内存区域,提高了内存利用率
c. 复制回收法:把现有内存空间分成两部分,gc运行时,它把可到达对象复制到另一半空间,再清空正在使用的空间的全部对象。这种方法适用于短生存期的对象,持续复制长生存期的对象则导致效率降低。
d. 分代回收发:把内存空间分为两个或者多个域,如年轻代和老年代,年轻代的特点是对象会很快被回收,因此在年轻代使用效率比较高的算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年的内存空间,老年代则采取标记-压缩算法
e. 引用计数(最简单古老的方法):指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程
f. 对象引用遍历(现在大多数 jvm 使用的方法):对象引用遍历从一组对象开始,沿着整个对象图上的每条链接,递归确定可到达(reachable)的对象。如果某对象不能从这些根对象的一个(至少一个)到达,则将它作为垃圾收集
垃圾回收机制有很多种,包括:分代复制垃圾回收、标记垃圾回收、增量垃圾回收等方式。标准的
Java进程既有栈又有堆。栈保存了原始型局部变量,堆保存了要创建的对象。Java平台对堆内存回收和再
利用的基本算法被称为标记和清除,但是Java对其进行了改进,采用“分代式垃圾收集”。这种方法会跟
Java对象的生命周期将堆内存划分为不同的区域,在垃圾收集过程中,可能会将对象移动到不同区域:
- 伊甸园(Eden):这是对象最初诞生的区域,并且对大多数对象来说,这里是它们唯一存在过的区域。
- 幸存者乐园(Survivor):从伊甸园幸存下来的对象会被挪到这里。
- 终身颐养园(Tenured):这是足够老的幸存对象的归宿。年轻代收集(Minor-GC)过程是不会触及这
个地方的。当年轻代收集不能把对象放进终身颐养园时,就会触发一次完全收集(Major-GC),这里可能
还会牵扯到压缩,以便为大对象腾出足够的空间。
-Xms / -Xmx — 堆的初始大小 / 堆的最大大小
-Xmn — 堆中年轻代的大小
-XX:-DisableExplicitGC — 让System.gc()不产生任何作用
-XX:+PrintGCDetails — 打印GC的细节
-XX:+PrintGCDateStamps — 打印GC操作的时间戳
-XX:NewSize / XX:MaxNewSize — 设置新生代大小/新生代最大大小
-XX:NewRatio — 可以设置老生代和新生代的比例
-XX:PrintTenuringDistribution — 设置每次新生代GC后输出幸存者乐园中对象年龄的分布
-XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:设置老年代阀值的初始值和最大值
-XX:TargetSurvivorRatio:设置幸存区的目标使用率
具体还请度娘
谈谈你对JVM的认识?
http://blog.csdn.net/huachao1001/article/details/51533132
Java类生命周期
Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载七个阶段。
JVM类加载
类装载器就是寻找类的节码文件并构造出类在JVM内部表示对象的组件。在Java中,类装载器把一个类装入JVM中,要经过以下步骤:
[1.]装载:查找和导入Class文件;
[2.]链接:执行校验、准备和解析步骤,其中解析步骤是可以选择的:
[2.1]校验:检查载入Class文件数据的正确性;
[2.2]准备:给类的静态变量分配存储空间;
[2.3]解析:将符号引用转成直接引用;
[3.]3.初始化:对类的静态变量、静态代码块执行初始化工作。
类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的Java运行时系统组件,它负责在运行时查找和装入Class字节码文件。JVM在运行时会产生三个ClassLoader:根装载器、ExtClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。其中,根装载器不是ClassLoader的子类,它使用C++编写,因此我们在Java中看不到它,根装载器负责装载JRE的核心类库,如JRE目标下的rt.jar、charsets.jar等。ExtClassLoader和AppClassLoader都是ClassLoader的子类。其中ExtClassLoader负责装载JRE扩展目录ext中的JAR类包;AppClassLoader负责装载Classpath路径下的类包。
这三个类装载器之间存在父子层级关系,即根装载器是ExtClassLoader的父装载器,ExtClassLoader是AppClassLoader的父装载器。默认情况下,使用AppClassLoader装载应用程序的类,我们可以做一个实验:
public class testSpringAOP {
public static void main(String[] args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent());
}
}
结果:
sun.misc.Launcher$AppClassLoader@adb3f
sun.misc.Launcher$ExtClassLoader@c042ab
null
通过以上的输出信息,我们知道当前的ClassLoader是AppClassLoader,父ClassLoader是ExtClassLoader,祖父ClassLoader是根类装载器,因为在Java中无法获得它的句柄,所以仅返回null。
JVM装载类时使用“全盘负责委托机制”,“全盘负责”是指当一个ClassLoader装载一个类的时,除非显式地使用另一个ClassLoader,该类所依赖及引用的类也由这个ClassLoader载入;“委托机制”是指先委托父装载器寻找目标类,只有在找不到的情况下才从自己的类路径中查找并装载目标类。这一点是从安全角度考虑的,试想如果有人编写了一个恶意的基础类(如java.lang.String)并装载到JVM中将会引起多么可怕的后果。但是由于有了“全盘负责委托机制”,java.lang.String永远是由根装载器来装载的,这样就避免了上述事件的发生。
线程部分
1) 创建一个线程
public class testThread1 {
public static void main(String[] args) {
TestThread2 testThread2 = new TestThread2();
Thread thread = new Thread(testThread2);
thread.start();
}
}
class TestThread2 implements Runnable {
@Override
public void run() {
System.out.println(1);
}
}
定时器
import java.util.Timer;
import java.util.TimerTask;
public class Timer01 {
private static int i = 0;
public static void main(String[] args) {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("i ="+i);
i ++;
}
}, 0, 1000); // 0 是延时多少秒开始执行 1000是间隔多少秒开始执行
}
}
返回带有返回值的线程
public class Test01 {
public static void main(String[] args) throws Exception, ExecutionException {
Thread02 thread02 = new Thread02();
FutureTask<Integer> futureTask = new FutureTask<>(thread02);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
}
class Thread02 implements Callable<Integer>{
@Override
public Integer call() throws Exception {
return 2;
}
}
atomicInteger 有许多对多线程原子性操作的方法 ;
atomicInteger.getAndIncrement(); 获得这个值并且 +1;
而用volatile只能保证线程之间的可见性,不能保证原子性
即不进行操作只返回数值那么可以使用volatile (针对下面方法中);
package thread;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* atomicInteger 原子性操作 保证多线程唯一性
* @author Administrator
*
*/
public class Thread03 {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
private static int getNext() {
return atomicInteger.getAndIncrement();
}
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "---" + Thread03.getNext());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "---" + Thread03.getNext());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
使用Lock包 保证多线程安全性 可以自己加锁和释放锁 更加灵活 尽量放在finally 中执行 避免没有释放锁 Lock 还有公平锁机制
package thread;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadLock {
private int a ;
private Lock lock = new ReentrantLock();
public int incr () {
int c = 0;
try {
lock.lock();
c = a++;
} finally {
lock.unlock();
}
return c;
}
public static void main(String[] args) {
ThreadLock threadLock = new ThreadLock();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " -->" +threadLock.incr());
} catch (Exception e) {
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName() + " -->" +threadLock.incr());
} catch (Exception e) {
}
}
}
}).start();
}
}
定实现时器 Timer 和 TimerTask
Timer 在实际开发中应用场景不多,一般来说都会用其他第三方库来实现。但有时会在一些面试题中出现。
下面我们就针对一道面试题来使用 Timer 定时类。
1.请模拟写出双重定时器(面试题)
要求:使用定时器,间隔 4 秒执行一次,再间隔 2 秒执行一次,以此类推执行。
class TimerTastCus extends TimerTask{
@Override
public void run() {
count = (count +1)%2;
System.err.println("Boob boom ");
new Timer().schedule(new TimerTastCus(), 2000+2000*count);
}
}
Timer timer = new Timer();
timer.schedule(new TimerTastCus(), 2000+2000*count);
while (true) {
System.out.println(new Date().getSeconds());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//PS:下面的代码中的 count 变量中
//此参数要使用在你匿名内部类中,使用 final 修饰就无法对其值进行修改,
//只能改为静态变量
private static volatile int count = 0;
线程互斥与同步
在引入多线程后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用同一台
打印机,会使打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效的共享资源和相互合作,从而使程序的执行具有可再现性。
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在以下两种制约关系。
1. 间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享 CPU,共享 I/O 设备,所谓间接相
互制约即源于这种资源共享,打印机就是最好的例子,线程 A 在使用打印机时,其它线程都要等待。
2. 直接相互制约。这种制约主要是因为线程之间的合作,如有线程 A 将计算结果提供给线程 B 作进一步处理,
那么线程 B 在线程 A 将数据送达之前都将处于阻塞状态。
间接相互制约可以称为互斥,直接相互制约可以称为同步,对于互斥可以这样理解,线程 A 和线程 B 互斥访问某
个资源则它们之间就会产个顺序问题——要么线程 A 等待线程 B 操作完毕,要么线程 B 等待线程操作完毕,这其实就
是线程的同步了。因此同步包括互斥,互斥其实是一种特殊的同步。
下面我们通过一道面试题来体会线程的交互。
要求:子线程运行执行 10 次后,主线程再运行 5 次。这样交替执行三遍
public static void main(String[] args) {
final Bussiness bussiness = new Bussiness();
//子线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
bussiness.subMethod();
}
}
}).start();
//主线程
for (int i = 0; i < 3; i++) {
bussiness.mainMethod();
}
}
}
class Bussiness {
private boolean subFlag = true;
public synchronized void mainMethod() {
while (subFlag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()
+ " : main thread running loop count -- " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
subFlag = true;
notify();
}
public synchronized void subMethod()
{
while ( !subFlag )
{
try {
wait();
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
for ( int i = 0; i < 10; i++ )
{
System.err.println( Thread.currentThread().getName()
+ " : sub thread running loop count -- " + i );
try {
Thread.sleep( 1000 );
} catch ( InterruptedException e ) {
e.printStackTrace();
}
}
subFlag = false;
notify();
}
}
线程局部变量 ThreadLocal
ThreadLocal 的作用和目的:用于实现线程内的数据共享,即对于相同的程序代码,多个模块在同一个
线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
每个线程调用全局 ThreadLocal 对象的 set 方法,在 set 方法中,首先根据当前线程获取当前线程的
ThreadLocalMap 对象,然后往这个 map 中插入一条记录,key 其实是 ThreadLocal 对象,value 是各自的 set
方法传进去的值。也就是每个线程其实都有一份自己独享的 ThreadLocalMap对象,该对象的 Key 是 ThreadLocal
对象,值是用户设置的具体值。在线程结束时可以调用 ThreadLocal.remove()方法,这样会更快释放内存,不调
用也可以,因为线程结束后也可以自动释放相关的 ThreadLocal 变量。
ThreadLocal 的应用场景:
➢ 订单处理包含一系列操作:减少库存量、增加一条流水台账、修改总账,这几个操作要在同一个
事务中完成,通常也即同一个线程中进行处理,如果累加公司应收款的操作失败了,则应该把前面
的操作回滚,否则,提交所有操作,这要求这些操作使用相同的数据库连接对象,而这些操作的代码
分别位于不同的模块类中。
➢ 银行转账包含一系列操作: 把转出帐户的余额减少,把转入帐户的余额增加,这两个操作要在
同一个事务中完成,它们必须使用相同的数据库连接对象,转入和转出操作的代码分别是两个不同
的帐户对象的方法。
➢ 例如 Strut2 的 ActionContext,同一段代码被不同的线程调用运行时,该代码操作的数据是每
个线程各自的状态和数据,对于不同的线程来说,getContext 方法拿到的对象都不相同,对同一个
线程来说,不管调用 getContext 方法多少次和在哪个模块中 getContext 方法,拿到的都是同一
个。
ThreadLocal 的使用方式
(1) 在关联数据类中创建 private static ThreadLocal
在下面的类中,私有静态 ThreadLocal 实例(serialNum)为调用该类的静态 SerialNum.get() 方法的每个
线程维护了一个“***”,该方法将返回当前线程的***。(线程的***是在第一次调用 SerialNum.get() 时
分配的,并在后续调用中不会更改。)
public class SerialNum {
/* The next serial number to be assigned */
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal()
{
protected synchronized Object initialValue()
{
return(new Integer( nextSerialNum++ ) );
}
};
public static int get()
{
return( ( (Integer) (serialNum.get() ) ).intValue() );
}
}
另一个例子,也是私有静态 ThreadLocal 实例:
public class SerialNum {
/* The next serial number to be assigned */
private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal()
{
protected synchronized Object initialValue()
{
return(new Integer( nextSerialNum++ ) );
}
};
public static int get()
{
return( ( (Integer) (serialNum.get() ) ).intValue() );
}
}
在 Util 类中创建 ThreadLocal
这是上面用法的扩展,即把 ThreadLocal 的创建放到工具类中。
public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义 SessionFactory
static {
try {
// 通过默认配置文件 hibernate.cfg.xml 创建 SessionFactory
sessionFactory = new Configuration().configure()
.buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化 SessionFactory 失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量 session,用来保存 Hibernate 的 Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的 Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果 Session 还没有打开,则新开一个 Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的 Session 保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为 Session 类型
Session s = (Session) session.get();
session.set(null);
if (s != null) {
s.close();
}
}
}
在 Runnable 中创建 ThreadLocal
在线程类内部创建 ThreadLocal,基本步骤如下:
①、在多线程的类(如 ThreadDemo 类)中,创建一个 ThreadLocal 对象 threadXxx,用来保存线程间
需要隔离处理的对象 xxx。
②、在 ThreadDemo 类中,创建一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若
ThreadLocal 对象为 null 时候,应该 new()一个隔离访问类型的对象,并强制转换为要应用的类型
③、在 ThreadDemo 类的 run()方法中,通过调用 getXxx()方法获取要操作的数据,这样可以保证每个线
程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable{
ThreadLocal<Studen> studenThreadLocal = new ThreadLocal<Studen>();
@Override
public void run() {
String currentThreadName = Thread.currentThread().getName();
System.out.println(currentThreadName + " is running...");
Random random = new Random();
int age = random.nextint(100);
System.out.println(currentThreadName + " is set age: " + age);
Studen studen = getStudent();
//通过这个方法,为每个线程都独立的 new 一个 student 对象,每个线程的的
student 对象都可以设置不同的值
studen.setAge(age);
System.out.println(currentThreadName + " is first get age: " + studen.getAge());
try {
Thread.sleep(500);
}
catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( currentThreadName + " is second get age: " + studen.getAge());
}
private Studen getStudent() {
Studen studen = studenThreadLocal.get();
if (null == studen) {
studen = new Studen();
studenThreadLocal.set(studen);
}
return studen;
}
public static void main(String[] args) {
ThreadLocalTest t = new ThreadLocalTest();
Thread t1 = new Thread(t,"Thread A");
Thread t2 = new Thread(t,"Thread B");
t1.start();
t2.start();
}
}
class Studen{
int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
多线程共享数据
在 Java 传统线程机制中的共享数据方式,大致可以简单分两种情况:
➢ 多个线程行为一致,共同操作一个数据源。也就是每个线程执行的代码相同,可以使用同一个 Runnable 对
象,这个 Runnable 对象中有那个共享数据,例如,卖票系统就可以这么做。
➢ 多个线程行为不一致,共同操作一个数据源。也就是每个线程执行的代码不同,这时候需要用不同的
Runnable 对象。例如,银行存取款。
下面我们通过两个示例代码来分别说明这两种方式。
1. 多个线程行为一致共同操作一个数据
如果每个线程执行的代码相同,可以使用同一个 Runnable 对象,这个 Runnable 对象中有那个共享数据,例如,
买票系统就可以这么做。
/**
*共享数据类
**/
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
*多线程类
**/
class RunnableCusToInc implements Runnable{
private ShareData shareData;
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
/**
*测试方法
**/
public static void main(String[] args) {
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();
}
}
}
多个线程行为不一致共同操作一个数据
如果每个线程执行的代码不同,这时候需要用不同的 Runnable 对象,有如下两种方式来实现这些 Runnable 对
象之间的数据共享:
1) 将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个 Runnable 对象。每个线程对共享
数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
public static void main(String[] args) {
ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
if(i%2 == 0){
new Thread(new RunnableCusToInc(shareData),"Thread "+ i).start();
} else{
new Thread(new RunnableCusToDec(shareData),"Thread "+ i).start();
}
}
}
//封装共享数据类
class RunnableCusToInc implements Runnable{
//封装共享数据
private ShareData shareData;
public RunnableCusToInc(ShareData data) {
this.shareData = data;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
/**
*共享数据类
**/
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
将这些 Runnable 对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对
共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个
Runnable 对象调用外部类的这些方法。
public static void main(String[] args) {
//公共数据
final ShareData shareData = new ShareData();
for (int i = 0; i < 4; i++) {
if(i%2 == 0){
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.inc();
}
}
}
,"Thread "+ i).start();
} else{
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
shareData.dec();
}
}
}
,"Thread "+ i).start();
}
}
}
class ShareData{
private int num = 10 ;
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+": invoke inc method num =" + num);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void dec() {
num--;
System.err.println(Thread.currentThread().getName()+": invoke dec method num =" + num);
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
}
}
补充:上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到
那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的 Runnable 对象
作为外部类中的成员内部类或局部内部类。
总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容
易实现它们之间的同步互斥和通信。
多线程基础知识–线程并发库
Java 5 添加了一个新的包到 Java 平台,java.util.concurrent 包。这个包包含有一系列能够让 Java 的并发
编程变得更加简单轻松的类。在这个包被添加以前,你需要自己去动手实现自己的相关工具类。下面带你认识下
java.util.concurrent 包里的这些类,然后你可以尝试着如何在项目中使用它们。本文中将使用 Java 6 版本,我不确定这和 Java 5 版本里的是否有一些差异。我不会去解释关于 Java 并发的核心问题 – 其背后的原理,也就是说,
如果你对那些东西感兴趣,参考《Java 并发指南》。
( 1 ) Java 的线程并发库介绍
Java5 的多线程并有两个大发库在 java.util.concurrent 包及子包中,子包主要的包有一下两个
1) java.util.concurrent 包 (多线程并发库)
➢ java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建
java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、
高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性,后面、我们会做介
绍。
➢如果一些类名看起来相似,可能是因为 java.util.concurrent 中的许多概念源自 Doug Lea 的
util.concurrent 库。
2) java.util.concurrent.atomic 包 (多线程的原子性操作提供的工具类)
➢查看 atomic 包文档页下面的介绍,它可以对多线程的基本数据、数组中的基本数据和对象中的基本数据
进行多线程的操作(AtomicInteger、AtomicIntegerArray、AtomicIntegerFieldUpDater…)
➢通过如下两个方法快速理解 atomic 包的意义:
AtomicInteger 类的 boolean compareAndSet(expectedValue, updateValue);
AtomicIntegerArray 类的 int addAndGet(int i, int delta);
➢顺带解释 volatile 类型的作用,需要查看 java 语言规范。
volatile 修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。(具有可见性)
volatile 没有原子性。
3) java.util.concurrent.lock 包 (多线程的锁机制)
为锁和等待条件提供一个框架的接口和类,它不同于内置同步和监视器。该框架允许更灵活地使用锁和条件。
本包下有三大接口,下面简单介绍下:
➢Lock 接口:支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 handover-hand
和锁重排算法)中使用这些规则。主要的实现是 ReentrantLock。
➢ReadWriteLock 接口:以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实
现,即 ReentrantReadWriteLock,因为它适用于大部分的标准用法上下文。但程序员可以创建自己的、
适用于非标准要求的实现。
➢Condition 接口:描述了可能会与锁有关联的条件变量。这些变量在用法上与使用 Object.wait 访问的
隐式监视器类似,但提供了更强大的功能。需要特别指出的是,单个 Lock 可能与多个 Condition 对象关
联。为了避免兼容性问题,Condition 方法的名称与对应的 Object 版本中的不同。
( 2 ) Java 的并发库入门
下面我们将分别介绍 java.util.concurrent 包下的常用类的使用。
1) java.util.concurrent 包
java.util.concurrent 包描述:
在并发编程中很常用的实用工具类。此包包括了几个小的、已标准化的可扩展框架,以及一些提供有用功能
的类。此包下有一些组件,其中包括:
执行程序(线程池)
并发队列
同步器
并发 Collocation
下面我们将 java.util.concurrent 包下的组件逐一简单介绍:
A. 执行程序
➢Executors 线程池工厂类
首次我们来说下线程池的作用:
线程池作用就是限制系统中执行线程的数量。
根据系统的环境情况,可以自动或手动设置线程数量,达到运行的最佳效果;少了浪费了系统资源,多了
造成系统拥挤效率不高。用线程池控制线程数量,其他线程 排队等候。一个任务执行完毕,再从队列的中取
最前面的任务开始执行。若队列中没有等待进程,线程池的这一资源处于等待。当一个新任务需要运行时,如
果线程 池中有等待的工作线程,就可以开始运行了;否则进入等待队列。
为什么要用线程池:
减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为因为消耗过多的内存,而把服务
器累趴下(每个线程需要大约 1MB 内存,线程开的越多,消耗的内存也就越大,最后死机)
Executors 详解:
Java 里面线程池的*接口是 Executor,但是严格意义上讲 Executor 并不是一个线程池,而只是一个
执行线程的工具。真正的线程池接口是 ExecutorService。ThreadPoolExecutor 是 Executors 类的底层实
现。我们先介绍下 Executors。
线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反
复创建线程对象所带来的性能开销,节省了系统的资源。
Java5 中并发库中,线程池创建线程大致可以分为下面三种:
//创建固定大小的线程池
ExecutorService fPool = Executors.newFixedThreadPool(3);
//创建缓存大小的线程池
ExecutorService cPool = Executors.newCachedThreadPool();
//创建单一的线程池
ExecutorService sPool = Executors.newSingleThreadExecutor();
下面我们通过简单示例来分别说明:
固定大小连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
/**
* Java 线程:线程池-
*
* @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
//关闭线程池
pool.shutdown();
}
```
```
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
从上面的运行来看,我们 Thread 类都是在线程池中运行的,线程池在执行 execute 方法来执行 Thread 类
中的 run 方法。不管 execute 执行几次,线程池始终都会使用 2 个线程来处理。不会再去创建出其他线程来处理
run 方法执行。这就是固定大小线程池。
单任务连接池
我们将上面的代码
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以*队列方式来运行该线程。
ExecutorService pool = Executors.newSingleThreadExecutor();
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
运行结果看出,单任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。不管 execute 执行几次,线程池
始终都会使用单个线程来处理。
补充:在 java 的多线程中,一但线程关闭,就会成为死线程。关闭后死线程就没有办法在启动了。再次启动就会出现
异常信息:Exception in thread “main” java.lang.IllegalThreadStateException。那么如何解决这个问题呢?
我们这里就可以使用 Executors.newSingleThreadExecutor()来再次启动一个线程。(面试)
可变连接池
//创建一个可重用固定线程数的线程池
ExecutorService pool = Executors.newFixedThreadPool(2);
改为:
//创建一个使用单个 worker 线程的 Executor,以*队列方式来运行该线程。
ExecutorService pool = Executors.newCachedThreadPool();
运行结果:
pool-1-thread-5 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-4 正在执行。。。
pool-1-thread-3 正在执行。。。
pool-1-thread-2 正在执行。。。
运行结果看出,可变任务线程池在执行 execute 方法来执行 Thread 类中的 run 方法。这里 execute 执行多次,
线程池就会创建出多个线程来处理 Thread 类中 run 方法。所有我们看到连接池会根据执行的情况,在程序运行时创
建多个线程来处理,这里就是可变连接池的特点。
那么在上面的三种创建方式,Executors 还可以在执行某个线程时,定时操作。那么下面我们通过代码简单演示
下。
延迟连接池
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Java 线程:线程池-
*
* @author Administrator 2009-11-4 23:30:44
*/
public class Test {
public static void main(String[] args) {
//创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
//创建实现了 Runnable 接口对象,Thread 对象当然也实现了 Runnable 接口
Thread t1 = new MyThread();
Thread t2 = new MyThread();
Thread t3 = new MyThread();
Thread t4 = new MyThread();
Thread t5 = new MyThread();
//将线程放入池中进行执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
//使用定时执行风格的方法
pool.schedule(t4, 10, TimeUnit.MILLISECONDS);
//t4 和 t5 在 10 秒后执行
pool.schedule(t5, 10, TimeUnit.MILLISECONDS);
//关闭线程池
pool.shutdown();
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
}
}
运行结果:
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-1 正在执行。。。
pool-1-thread-2 正在执行。。。
➢ExecutorService 执行器服务
java.util.concurrent.ExecutorService 接口表示一个异步执行机制,使我们能够在后台执行任务。因此一
个 ExecutorService 很类似于一个线程池。实际上,存在于 java.util.concurrent 包里的 ExecutorService 实
现就是一个线程池实现。
ExecutorService 例子:
以下是一个简单的 ExecutorService 例子:
//线程工厂类创建出线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
//执行一个线程任务
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
}
);
//线程池关闭
executorService.shutdown();
上面代码首先使用 newFixedThreadPool() 工厂方法创建一个 ExecutorService。这里创建了一个十个线程执行任务
的线程池。然后,将一个 Runnable 接口的匿名实现类传递给 execute() 方法。这将导致 ExecutorService 中的某
个线程执行该 Runnable。这里可以看成一个任务分派,示例代码中的任务分派我们可以理解为:
一个线程将一个任务委派给一个 ExecutorService 去异步执行。
一旦该线程将任务委派给 ExecutorService,该线程将继续它自己的执行,独立于该任务的执行。
ExecutorService 实现:
既然 ExecutorService 是个接口,如果你想用它的话就得去使用它的实现类之一。
java.util.concurrent 包提供了 ExecutorService 接口的以下实现类:
ThreadPoolExecutor
ScheduledThreadPoolExecutor
ExecutorService 创建:
ExecutorService 的创建依赖于你使用的具体实现。但是你也可以使用 Executors 工厂类来创建
ExecutorService 实例。代码示例:
ExecutorService executorService1 = Executors.newSingleThreadExecutor();
//之前 Executors 已介绍
ExecutorService executorService2 = Executors.newFixedThreadPool(10);
ExecutorService executorService3 = Executors.newScheduledThreadPool(10);
ExecutorService 使用:
有几种不同的方式来将任务委托给 ExecutorService 去执行:
execute(Runnable)
submit(Runnable)
submit(Callable)
invokeAny(…)
invokeAll(…)
接下来我们挨个看一下这些方法。
✓ execute(Runnable)
execute(Runnable) 方法要求一个 java.lang.Runnable 对象,然后对它进行异步执行。以下是使用
ExecutorService 执行一个 Runnable 的示例:
//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
//执行 ExecutorService 中的方法
executorService.execute(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
}
);
//线程池关闭
executorService.shutdown();
特点:没有办法得知被执行的 Runnable 的执行结果。如果有需要的话你得使用一个 Callable(以下将做介
绍)。
✓ submit(Runnable)
submit(Runnable) 方法也要求一个 Runnable 实现类,但它返回一个 Future 对象。这个 Future 对象可
以用来检查 Runnable 是否已经执行完毕。以下是 ExecutorService submit() 示例:
//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Runnable() {
public void run() {
System.out.println("Asynchronous task");
}
}
);
future.get();
//获得执行完 run 方法后的返回值,这里使用的 Runnable,所以这里没有返回值,返回的是 null。
executorService.shutdown();
submit(Runnable)
submit(Callable) 方法类似于 submit(Runnable) 方法,除了它所要求的参数类型之外。Callable 实例除了
它的 call() 方法能够返回一个结果之外和一个 Runnable 很相像。Runnable.run() 不能够返回一个结果。
Callable 的结果可以通过 submit(Callable) 方法返回的 Future 对象进行获取。
以下是一个 ExecutorService Callable 示例:
//从 Executors 中获得 ExecutorService
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future = executorService.submit(new Callable(){
public Object call() throws Exception {
System.out.println("Asynchronous Callable");
return "Callable Result";
}
}
);
System.out.println("future.get() = " + future.get());
executorService.shutdown();
输出:
Asynchronous Callable
future.get() = Callable Result
invokeAny()
invokeAny() 方法要求一系列的 Callable 或者其子接口的实例对象。调用这个方法并不会返回一个 Future,
但它返回其中一个 Callable 对象的结果。无法保证返回的是哪个 Callable 的结果 – 只能表明其中一个已
执行结束。
如果其中一个任务执行结束(或者抛了一个异常),其他 Callable 将被取消。以下是示例代码:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
}
);
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
}
);
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
}
);
String result = executorService.invokeAny(callables);
System.out.println("result = " + result);
executorService.shutdown();
上述代码将会打印出给定 Callable 集合中的一个的执行结果。我自己试着执行了它几次,结果始终在变。有
时是 “Task 1″,有时是 “Task 2″ 等等。
✓ invokeAll()
invokeAll() 方法将调用你在集合中传给 ExecutorService 的所有 Callable 对象。invokeAll() 返回一系列
的 Future 对象,通过它们你可以获取每个 Callable 的执行结果。
记住,一个任务可能会由于一个异常而结束,因此它可能没有 “成功”。无法通过一个 Future 对象来告知我
们是两种结束中的哪一种。
以下是一个代码示例:
ExecutorService executorService = Executors.newSingleThreadExecutor();
Set<Callable<String>> callables = new HashSet<Callable<String>>();
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 1";
}
}
);
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 2";
}
}
);
callables.add(new Callable<String>() {
public String call() throws Exception {
return "Task 3";
}
}
);
List<Future<String>> futures = executorService.invokeAll(callables);
for (Future<String> future : futures){
System.out.println("future.get = " + future.get());
}
executorService.shutdown();
输出结果:
future.get = Task 3
future.get = Task 1
future.get = Task 2
Executors 关闭:
使用 shutdown 和 shutdownNow 可以关闭线程池
两者的区别:
shutdown 只是将空闲的线程 interrupt() 了,shutdown()之前提交的任务可以继续执行直到结束。
shutdownNow 是 interrupt 所有线程, 因此大部分线程将立刻被中断。之所以是大部分,而不是全部 ,
是因为 interrupt()方法能力有限。
ThreadPoolExecutor 线程池执行者
java.util.concurrent.ThreadPoolExecutor 是 ExecutorService 接口的一个实现。ThreadPoolExecutor 使用
其内部池中的线程执行给定任务(Callable 或者 Runnable)。
ThreadPoolExecutor 包含的线程池能够包含不同数量的线程。池中线程的数量由以下变量决定:
corePoolSize
maximumPoolSize
当一个任务委托给线程池时,如果池中线程数量低于 corePoolSize,一个新的线程将被创建,即使池中可能尚有
空 闲 线 程 。 如 果 内 部 任 务 队 列 已 满 , 而 且 有 至 少 corePoolSize 正 在 运 行 , 但 是 运 行 线 程 的 数 量 低 于
maximumPoolSize,一个新的线程将被创建去执行该任务。
ThreadPoolExecutor 图解:
创建 ThreadPoolExecutor:
int corePoolSize = 5;
int maxPoolSize = 10;
long keepAliveTime = 5000;
ExecutorService threadPoolExecutor =
new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
keepAliveTime,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
);
构造方法参数列表解释:
corePoolSize - 池中所保存的线程数,包括空闲线程。
maximumPoolSize - 池中允许的最大线程数。
keepAliveTime - 当线程数大于核心时,此为终止前多余的空闲线程等待新任务的最长时间。
unit - keepAliveTime 参数的时间单位。
workQueue - 执行前用于保持任务的队列。此队列仅保持由 execute 方法提交的 Runnable 任务。
ScheduledPoolExecutor 定时线程池执行者
java.util.concurrent.ScheduledExecutorService 是一个 ExecutorService, 它能够将任务延后执行,或者间隔
固定时间多次执行。 任务由一个工作者线程异步执行,而不是由提交任务给 ScheduledExecutorService 的那个线
程执行。
ScheduledPoolExecutor 例子:
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
}
,
5,
TimeUnit.SECONDS);
//5 秒后执行
首先一个内置 5 个线程的 ScheduledExecutorService 被创建。之后一个 Callable 接口的匿名类示例被创建
然后传递给 schedule() 方法。后边的俩参数定义了 Callable 将在 5 秒钟之后被执行。
ScheduledExecutorService 的实现:
ScheduledExecutorService 是一个接口,你要用它的话就得使用 java.util.concurrent 包里对它的某个实现类。
ScheduledExecutorService 具有以下实现类:ScheduledThreadPoolExecutor
创建一个 ScheduledExecutorService:
如何创建一个 ScheduledExecutorService 取决于你采用的它的实现类。但是你也可以使用 Executors 工厂类
来创建一个 ScheduledExecutorService 实例。比如:
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
ScheduledExecutorService 的使用:
一旦你创建了一个 ScheduledExecutorService,你可以通过调用它的以下方法:
schedule (Callable task, long delay, TimeUnit timeunit)
schedule (Runnable task, long delay, TimeUnit timeunit)
scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
下面我们就简单看一下这些方法。
✓ schedule (Callable task, long delay, TimeUnit timeunit)
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
ScheduledFuture scheduledFuture =
scheduledExecutorService.schedule(new Callable() {
public Object call() throws Exception {
System.out.println("Executed!");
return "Called!";
}
}
,
5,
TimeUnit.SECONDS);
System.out.println("result = " + scheduledFuture.get());
scheduledExecutorService.shutdown();
输出结果:
Executed!
result = Called!
schedule (Runnable task, long delay, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之
后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到
ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一
时间不会有多个线程同时执行。
✓ scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)
这一方法规划一个任务将被定期执行。该任务将会在首个 initialDelay 之后得到执行,然后每个 period 时间之
后重复执行。
如果给定任务的执行抛出了异常,该任务将不再执行。如果没有任何异常的话,这个任务将会持续循环执行到
ScheduledExecutorService 被关闭。
如果一个任务占用了比计划的时间间隔更长的时候,下一次执行将在当前执行结束执行才开始。计划任务在同一
时间不会有多个线程同时执行。
scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)
除了 period 有不同的解释之外这个方法和 scheduleAtFixedRate() 非常像。scheduleAtFixedRate() 方法中,
period 被解释为前一个执行的开始和下一个执行的开始之间的间隔时间。而在本方法中,period 则被解释为前一个
执行的结束和下一个执行的结束之间的间隔。因此这个延迟是执行结束之间的间隔,而不是执行开始之间的间隔。
ScheduledExecutorService 的关闭:
正如 ExecutorService,在你使用结束之后你需要把 ScheduledExecutorService 关闭掉。否则他将导致 JVM
继续运行,即使所有其他线程已经全被关闭。
你 可 以 使 用 从 ExecutorService 接 口 继 承 来 的 shutdown() 或 shutdownNow() 方 法 将
ScheduledExecutorService 关闭。参见 ExecutorService 关闭部分以获取更多信息。
➢ForkJoinPool 合并和分叉(线程池)
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一点不同。ForkJoinPool 让我们可以
很方便地把任务分裂成几个更小的任务,这些分裂出来的任务也将会提交给 ForkJoinPool。任务可以继续分割成更小
的子任务,只要它还能分割。可能听起来有些抽象,因此本节中我们将会解释 ForkJoinPool 是如何工作的,还有任
务分割是如何进行的。
合并和分叉的解释:
在我们开始看 ForkJoinPool 之前我们先来简要解释一下分叉和合并的原理。分叉和合并原理包含两个递归进行
的步骤。两个步骤分别是分叉步骤和合并步骤。
分叉:
一个使用了分叉和合并原理的任务可以将自己分叉(分割)为更小的子任务,这些子任务可以被并发执行。
通过把自己分割成多个子任务,每个子任务可以由不同的 CPU 并行执行,或者被同一个 CPU 上的不同线程执
行。只有当给的任务过大,把它分割成几个子任务才有意义。把任务分割成子任务有一定开销,因此对于小型任务,这
个分割的消耗可能比每个子任务并发执行的消耗还要大。
什么时候把一个任务分割成子任务是有意义的,这个界限也称作一个阀值。这要看每个任务对有意义阀值的决定。
很大程度上取决于它要做的工作的种类。
合并:
当一个任务将自己分割成若干子任务之后,该任务将进入等待所有子任务的结束之中。一旦子任务执行结束,该
任务可以把所有结果合并到同一个结果。图示如下:
当然,并非所有类型的任务都会返回一个结果。如果这个任务并不返回一个结果,它只需等待所有子任务执行完
毕。也就不需要结果的合并啦。
所以我们可以将 ForkJoinPool 是一个特殊的线程池,它的设计是为了更好的配合 分叉-和-合并 任务分割的工
作。ForkJoinPool 也在 java.util.concurrent 包中,其完整类名为 java.util.concurrent.ForkJoinPool。
创建一个 ForkJoinPool:
你可以通过其构造子创建一个 ForkJoinPool。作为传递给 ForkJoinPool 构造子的一个参数,你可以定义你期望
的并行级别。并行级别表示你想要传递给 ForkJoinPool 的任务所需的线程或 CPU 数量。以下是一个 ForkJoinPool
示例:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
提交任务到 ForkJoinPool:
就像提交任务到 ExecutorService 那样,把任务提交到 ForkJoinPool。你可以提交两种类型的任务。一种是没
有任何返回值的(一个 “行动”),另一种是有返回值的(一个”任务”)。这两种类型分别由 RecursiveAction 和
RecursiveTask 表示。接下来介绍如何使用这两种类型的任务,以及如何对它们进行提交。
RecursiveAction:
RecursiveAction 是一种没有任何返回值的任务。它只是做一些工作,比如写数据到磁盘,然后就退出了。一个
RecursiveAction 可以把自己的工作分割成更小的几块,这样它们可以由独立的线程或者 CPU 执行。你可以通过继
承来实现一个 RecursiveAction。示例如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {
private long workLoad = 0;
public MyRecursiveAction(long workLoad) {
this.workLoad = workLoad;
}
@Override
protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
//翻译:如果工作超过门槛,把任务分解成更小的任务
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
subtasks.addAll(createSubtasks());
for (RecursiveAction subtask : subtasks){
subtask.fork();
}
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
}
}
private List<MyRecursiveAction> createSubtasks() {
List<MyRecursiveAction> subtasks =
new ArrayList<MyRecursiveAction>();
MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
例子很简单。MyRecursiveAction 将一个虚构的 workLoad 作为参数传给自己的构造子。如果 workLoad 高于
一个特定阀值,该工作将被分割为几个子工作,子工作继续分割。如果 workLoad 低于特定阀值,该工作将由
MyRecursiveAction 自己执行。你可以这样规划一个 MyRecursiveAction 的执行:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个没有返回值的任务
MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool 执行任务
forkJoinPool.invoke(myRecursiveAction);
运行结果:
Splitting workLoad : 24
Doing workLoad myself: 12
RecursiveTask:
RecursiveTask 是一种会返回结果的任务。它可以将自己的工作分割为若干更小任务,并将这些子任务的执行结
果合并到一个集体结果。可以有几个水平的分割和合并。以下是一个 RecursiveTask 示例:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<long> {
private long workLoad = 0;
public MyRecursiveTask(long workLoad) {
this.workLoad = workLoad;
}
protected long compute() {
//if work is above threshold, break tasks up into smaller tasks
if(this.workLoad > 16) {
System.out.println("Splitting workLoad : " + this.workLoad);
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
subtasks.addAll(createSubtasks());
for (MyRecursiveTask subtask : subtasks){
subtask.fork();
}
long result = 0;
for (MyRecursiveTask subtask : subtasks) {
result += subtask.join();
}
return result;
} else {
System.out.println("Doing workLoad myself: " + this.workLoad);
return workLoad * 3;
}
}
private List<MyRecursiveTask> createSubtasks() {
List<MyRecursiveTask> subtasks =
new ArrayList<MyRecursiveTask>();
MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
subtasks.add(subtask1);
subtasks.add(subtask2);
return subtasks;
}
}
除 了 有 一 个 结 果 返 回 之 外 , 这 个 示 例 和 RecursiveAction 的 例 子 很 像 。 MyRecursiveTask 类 继 承 自
RecursiveTask,这也就意味着它将返回一个 Long 类型的结果。
MyRecursiveTask 示例也会将工作分割为子任务,并通过 fork() 方法对这些子任务计划执行。此外,本示例还
通过调用每个子任务的 join() 方法收集它们返回的结果。子任务的结果随后被合并到一个更大的结果,并最终将其返
回。对于不同级别的递归,这种子任务的结果合并可能会发生递归。
你可以这样规划一个 RecursiveTask:
//创建了一个并行级别为 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//创建一个有返回值的任务
MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
//线程池执行并返回结果
long mergedResult = forkJoinPool.invoke(myRecursiveTask);
System.out.println("mergedResult = " + mergedResult);
注意: ForkJoinPool.invoke() 方法的调用来获取最终执行结果的。
B. 并发队列-阻塞队列
常用的并发队列有阻塞队列和非阻塞队列,前者使用锁实现,后者则使用 CAS 非阻塞算法实现。
PS:至于非阻塞队列是靠 CAS 非阻塞算法,在这里不再介绍,大家只用知道,Java 非阻塞队列是使用 CAS 算法来实现的就可
以。感兴趣的童鞋可以维基网上自行学习.
下面我们先介绍阻塞队列。
阻塞队列:
阻塞队列 (BlockingQueue)是 Java util.concurrent 包下重要的数据结构,BlockingQueue 提供了线程安全的队
列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如
果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于 BlockingQueue 实现的。
➢BlockingQueue 阻塞队列
BlockingQueue 通常用于一个线程生产对象,而另外一个线程消费这些对象的场景。下图是对这个原理的阐述:
一个线程往里边放,另外一个线程从里边取的一个 BlockingQueue。
一个线程将会持续生产新对象并将其插入到队列之中,直到队列达到它所能容纳的临界点。也就是说,它是有限
的。如果该阻塞队列到达了其临界点,负责生产的线程将会在往里边插入新对象时发生阻塞。它会一直处于阻塞之中,
直到负责消费的线程从队列中拿走一个对象。负责消费的线程将会一直从该阻塞队列中拿出对象。如果消费线程尝试
去从一个空的队列中提取对象的话,这个消费线程将会处于阻塞之中,直到一个生产线程把一个对象丢进队列。
BlockingQueue 的方法:
BlockingQueue 具有 4 组不同的方法用于插入、移除以及对队列中的元素进行检查。如果请求的操作不能得到
立即执行的话,每个方法的表现也不同。这些方法如下:
阻塞队列提供了四种处理方法:
四组不同的行为方式解释:
抛异常:如果试图的操作无法立即执行,抛一个异常。
特定值:如果试图的操作无法立即执行,返回一个特定的值(常常是 true / false)。
阻塞:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行。
超时:如果试图的操作无法立即执行,该方法调用将会发生阻塞,直到能够执行,但等待时间不会超过给定
值。返回一个特定值以告知该操作是否成功(典型的是 true / false)。
无法向一个 BlockingQueue 中插入 null。如果你试图插入 null,BlockingQueue 将会抛出一个
NullPointerException.
BlockingQueue 的实现类:
BlockingQueue 是个接口,你需要使用它的实现之一来使用 BlockingQueue,Java.util.concurrent 包下具有以
下 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 是一个特殊的队列,它的内部同时只能够容纳单个元素。
如果该队列已有一元素的话,试图向队列中插入一个新元素的线程将会阻塞,直到另一个线程将该元素从队
列中抽走。同样,如果该队列为空,试图向队列中抽取一个元素的线程将会阻塞,直到另一个线程向队列中插
入了一条新的元素。据此,把这个类称作一个队列显然是夸大其词了。它更多像是一个汇合点。
下一篇: Install Scala SBT