http://www.cnblogs.com/cccw/p/5837448.html
1.集合包
List最常用的实现类有:ArrayList、LinkedList、Vector及Stack;Set接口常用的实现类有:HashSet、TreeSet。
1.1 ArrayList
插入对象:add(E)
获取单个对象:get(int)
遍历对象:iterator()
判断对象是否存在:contains(E)
1.2 LinkedList
总结:
1.3 Vector
1.4 Stack
1.5 HashSet
add(E):调用HashMap的put方法来完成此操作,将需要增加的元素作为Map中的key,value则传入一个之前已创建的Object对象。
remove(E):调用HashMap的remove(E)方法完成此操作。
contains(E):HashMap的containsKey
iterator():调用HashMap的keySet的iterator方法。
HashSet不支持通过get(int)获取指定位置的元素,只能自行通过iterator方法来获取。
总结:
1.6 TreeSet
1.7
当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把Map集合中的value当成key的附属,当系统决定了key的存储位置之后,value随之保存在那里即可。get取值也是根据key的hashCode确定在数组的位置,在根据key的equals确定在链表处的位置。
1 while (capacity < initialCapacity) 2 capacity <<= 1;
以上代码保证了初始化时HashMap的容量总是2的n次方,即底层数组的长度总是为2的n次方。它通过h & (table.length -1)
扩容resize():
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。
负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。负载因子越大,对空间的利用更充分,然而后果是查找效率的降低;如果负载因子太小,那么散列表的数据将过于稀疏,对空间造成严重浪费。
HashMap的实现中,通过threshold字段来判断HashMap的最大容量。threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。
initialCapacity*2,成倍扩大容量,HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个
在高并发时可以使用ConcurrentHashMap,其内部使用锁分段技术,维持这锁Segment的数组,在数组中又存放着Entity[]数组,内部hash算法将数据较均匀分布在不同锁中。
总结:
详细说明:http://zhangshixi.iteye.com/blog/672697
1.8 TreeMap
2.并发包
Java内存模型
代码顺序规则:
volatile变量规则:
传递性:
volatile
当我们声明共享变量为volatile后,对这个变量的读/写将会很特别。理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。
监视器锁的happens-before规则保证释放监视器和获取监视器的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
简而言之,volatile变量自身具有下列特性:
-
可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
-
原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。
volatile写的内存语义如下:
-
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
volatile读的内存语义如下:
-
当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。
下面对volatile写和volatile读的内存语义做个总结:
-
线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。
-
线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。
-
线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息。
锁释放-获取与volatile的读写具有相同的内存语义,
锁释放的内存语义如下:
锁获取的内存语义如下:
下面对锁释放和锁获取的内存语义做个总结:
-
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
-
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
-
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
示例:
1 class VolatileExample { 2 int x = 0; 3 volatile int b = 0; 4 5 private void write() { 6 x = 5; 7 b = 1; 8 } 9 10 private void read() { 11 int dummy = b; 12 while (x != 5) { 13 } 14 } 15 16 public static void main(String[] args) throws Exception { 17 final VolatileExample example = new VolatileExample(); 18 Thread thread1 = new Thread(new Runnable() { 19 public void run() { 20 example.write(); 21 } 22 }); 23 Thread thread2 = new Thread(new Runnable() { 24 public void run() { 25 example.read(); 26 } 27 }); 28 thread1.start(); 29 thread2.start(); 30 thread1.join(); 31 thread2.join(); 32 } 33 }
若thread1先于thread2执行,则程序执行流程分析如上图所示,thread2读的结果是dummy=1,x=5所以不会进入死循环。
但并不能保证两线程的执行顺序,若thread2先于thread1执行,则程序在两线程join中断之前的结果为:因为b变量的类型是volatile,故thread1写之后,thread2即可读到b变量的值发生变化,
而x是普通变量,故最后情况是dummy=1,但thread2的读操作因为x=0而进入死循环中。
AbstractQueuedSynchroniz
-
-
-
java.util.concurrent.locks.AbstractQueuedSynchroniz
er.compareAndSetState(int, int)
子类推荐被定义为自定义同步装置的内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干acquire之类的方法来供使用。该同步器即可以作为排他模式也可以作为共享模式,当它被定义为一个排他模式时,其他线程对其的获取就被阻止,而共享模式对于多个线程获取都可以成功。
同步器的开始提到了其实现依赖于一个FIFO队列,那么队列中的元素Node就是保存着线程引用和线程状态的容器,每个线程对同步器的访问,都可以看做是队列中的一个节点。
对于一个独占锁的获取和释放有如下伪码可以表示:
获取一个排他锁
释放一个排他锁
1 if (释放成功) { 2 删除头结点 3 激活原头结点的后继节点 4 }
示例:
下面通过一个排它锁的例子来深入理解一下同步器的工作原理,而只有掌握同步器的工作原理才能更加深入了解其他的并发组件。
排他锁的实现,一次只能一个线程获取到锁:
1 public class Mutex implements Lock, java.io.Serializable { 2 // 内部类,自定义同步器 3 private static class Sync extends AbstractQueuedSynchronizer { 4 // 是否处于占用状态 5 protected boolean isHeldExclusively() { 6 return getState() == 1; 7 } 8 // 当状态为0的时候获取锁 9 public boolean tryAcquire(int acquires) { 10 assert acquires == 1; // Otherwise unused 11 if (compareAndSetState(0, 1)) { 12 setExclusiveOwnerThread(Thread.currentThread()); 13 return true; 14 } 15 return false; 16 } 17 // 释放锁,将状态设置为0 18 protected boolean tryRelease(int releases) { 19 assert releases == 1; // Otherwise unused 20 if (getState() == 0) throw new IllegalMonitorStateExcep tion(); 21 setExclusiveOwnerThread(null); 22 setState(0); 23 return true; 24 } 25 // 返回一个Condition,每个condition都包含了一个condition队列 26 Condition newCondition() { return new ConditionObject(); } 27 } 28 // 仅需要将操作代理到Sync上即可 29 private final Sync sync = new Sync(); 30 public void lock() { sync.acquire(1); } 31 public boolean tryLock() { return sync.tryAcquire(1); } 32 public void unlock() { sync.release(1); } 33 public Condition newCondition() { return sync.newCondition(); } 34 public boolean isLocked() { return sync.isHeldExclusively(); } 35 public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); } 36 public void lockInterruptibly() throws InterruptedException { 37 sync.acquireInterruptibly(1); 38 } 39 public boolean tryLock(long timeout, TimeUnit unit) 40 throws InterruptedException { 41 return sync.tryAcquireNanos(1, unit.toNanos(timeout)); 42 } 43 }
可以看到Mutex将Lock接口均代理给了同步器的实现。使用方将Mutex构造出来后,调用lock获取锁,调用unlock将锁释放。
获取锁,acquire(int arg)的主要逻辑包括:
1. 尝试获取(调用tryAcquire更改状态,需要保证原子性);
2. 如果获取不到,将当前线程构造成节点Node并加入sync队列;
3. 再次尝试获取,如果没有获取到那么将当前线程从线程调度器上摘下,进入等待状态。
释放锁,release(int arg)的主要逻辑包括:
1. 尝试释放状态;
2. 唤醒当前节点的后继节点所包含的线程。
回顾整个资源的获取和释放过程:
在获取时,维护了一个sync队列,每个节点都是一个线程在进行自旋,而依据就是自己是否是首节点的后继并且能够获取资源;
在释放时,仅仅需要将资源还回去,然后通知一下后继节点并将其唤醒。
这里需要注意,队列的维护(首节点的更换)是依靠消费者(获取时)来完成的,也就是说在满足了自旋退出的条件时的一刻,这个节点就会被设置成为首节点。
队列里的节点线程的禁用和唤醒是通过LockSupport的park()及unpark(),调用的unsafe、底层也是native的实现。
关于java lock的浅析可见:http://jm-blog.aliapp.com/?p=414
共享模式和以上的独占模式有所区别,分别调用acquireShared(int arg)和releaseShared(int arg)获取共享模式的状态。
以文件的查看为例,如果一个程序在对其进行读取操作,那么这一时刻,对这个文件的写操作就被阻塞,相反,这一时刻另一个程序对其进行同样的读操作是可以进行的。如果一个程序在对其进行写操作,
那么所有的读与写操作在这一时刻就被阻塞,直到这个程序完成写操作。
以读写场景为例,描述共享和独占的访问模式,如下图所示:
上图中,红色代表被阻塞,绿色代表可以通过。
在上述对同步器AbstractQueuedSynchroniz
设计一个同步工具,该工具在同一时刻,只能有两个线程能够并行访问,超过限制的其他线程进入阻塞状态。
对于这个需求,可以利用同步器完成一个这样的设定,定义一个初始状态,为2,一个线程进行获取那么减1,一个线程释放那么加1,状态正确的范围在[0,1,2]三个之间,当在0时,代表再有新的线程对资源进行获取时只能进入阻塞状态(注意在任何时候进行状态变更的时候均需要以CAS作为原子性保障)。由于资源的数量多于1个,同时可以有两个线程占有资源,因此需要实现tryAcquireShared和tryReleaseShared方法。
1 public class TwinsLock implements Lock { 2 private final Sync sync = new Sync(2); 3 4 private static final class Sync extends AbstractQueuedSynchronizer { 5 private static final long serialVersionUID = -7889272986162341211L; 6 7 Sync(int count) { 8 if (count <= 0) { 9 throw new IllegalArgumentException ("count must large than zero."); 10 } 11 setState(count); 12 } 13 14 public int tryAcquireShared(int reduceCount) { 15 for (;;) { 16 int current = getState(); 17 int newCount = current - reduceCount; 18 if (newCount < 0 || compareAndSetState(current, newCount)) { 19 return newCount; 20 } 21 } 22 } 23 24 public boolean tryReleaseShared(int returnCount) { 25 for (;;) { 26 int current = getState(); 27 int newCount = current + returnCount; 28 if (compareAndSetState(current, newCount)) { 29 return true; 30 } 31 } 32 } 33 } 34 35 public void lock() { 36 sync.acquireShared(1); 37 } 38 39 public void lockInterruptibly() throws InterruptedException { 40 sync.acquireSharedInterruptib ly(1); 41 } 42 43 public boolean tryLock() { 44 return sync.tryAcquireShared(1) >= 0; 45 } 46 47 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { 48 return sync.tryAcquireSharedNanos(1, unit.toNanos(time)); 49 } 50 51 public void unlock() { 52 sync.releaseShared(1); 53 } 54 55 public Condition newCondition() { 56 return null; 57 } 58 }
这里我们编写一个测试来验证TwinsLock是否能够正常工作并达到预期。
1 public class TwinsLockTest { 2 3 @Test 4 public void test() { 5 final Lock lock = new TwinsLock(); 6 7 class Worker extends Thread { 8 public void run() { 9 while (true) { 10 lock.lock(); 11 12 try { 13 Thread.sleep(1000L); 14 System.out.println(Thread.currentThread()); 15 Thread.sleep(1000L); 16 } catch (Exception ex) { 17 18 } finally { 19 lock.unlock(); 20 } 21 } 22 } 23 } 24 25 for (int i = 0; i < 10; i++) { 26 Worker w = new Worker(); 27 w.start(); 28 } 29 30 new Thread() { 31 public void run() { 32 while (true) { 33 34 try { 35 Thread.sleep(200L); 36 System.out.println(); 37 } catch (Exception ex) { 38 39 } 40 } 41 } 42 }.start(); 43 44 try { 45 Thread.sleep(20000L); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 }
上述测试用例的逻辑主要包括:
1. 打印线程
Worker在两次睡眠之间打印自身线程,如果一个时刻只能有两个线程同时访问,那么打印出来的内容将是成对出现。
2. 分隔线程
不停的打印换行,能让Worker的输出看起来更加直观。
该测试的结果是在一个时刻,仅有两个线程能够获得到锁,并完成打印,而表象就是打印的内容成对出现。
利用CAS(compare and set)是不会进行阻塞的,只会一个返回成功,一个返回失败,保证了一致性。
CAS操作同时具有volatile读和volatile写的内存语义。
AQS这部分转载于http://ifeve.com/introduce-abstractqueuedsynchroniz
put操作:并没有在此方法上加上synchronized,首先对key.hashcode进行hash操作,得到key的hash值。hash操作的算法和map也不同,根据此hash值计算并获取其对应的数组中的Segment对象(继承自ReentrantLock),接着调用此Segment对象的put方法来完成当前操作。
ConcurrentHashMap基于concurrencyLevel划分出了多个Segment来对key-value进行存储,从而避免每次put操作都得锁住整个数组。在默认的情况下,最佳情况下可允许16个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key)
2.2 ReentrantLock
ReentrantLock的实现不仅可以替代隐式的synchronized关键字,而且能够提供超过关键字本身的多种功能。
2.3 Condition
newCondition的实现。
ReentrantLock.newCondition()
ReentrantLock.newCondition().await()
ReentrantLock.newCondition().signal()
2.4 CopyOnWriteArrayList
CopyOnWriteArrayList()
add(E)
新增加的对象放入数组末尾,最后做引用切换将新创建的数组对象赋值给全局的数组对象。
remove(E)
一个元素,如最后一个元素等于要删除的元素,即将当前数组对象赋值为新创建的数组对象,完成删除操作,如最后一个元素也不等于要删除的元素,那么返回false。
get(int)
iterator()
2.5 CopyOnWriteArraySet
2.6 ArrayBlockingQueue
2.7 ThreadPoolExecutor
与每次需要时都创建线程相比,线程池可以降低创建线程的开销,在线程执行结束后进行的是回收操作,提高对线程的复用。Java中主要使用的线程池是ThreadPoolExecutor,此外还有定时的线程池ScheduledThreadPoolExecu
Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类:
ExecutorService | 真正的线程池接口 |
ScheduledExecutorService | 和Time/TimeTask类似,解决需要任务重复执行的问题 |
ThreadPoolExecutor | ExecutorService的默认实现 |
SchedulesThreadPoolExecu | 继承ThreadPoolExecutor的ScheduledExecutorService |
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。
1. newSingleThreadExecutor
创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
2.newFixedThreadPool
创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3. newCachedThreadPool
创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,
那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
4.newScheduledThreadPool
创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
PS:但需要注意使用,newSingleThreadExecutor和newFixedThreadPool将超过处理的线程放在队列中,但工作线程较多时,会引起过多内存被占用,而后两者返回的线程池是没有线程上线的,所以在使用时需要当心,创建过多的线程容易引起服务器的宕机。
使用ThreadPoolExecutor自定义线程池,具体使用时需根据系统及JVM的配置设置适当的参数,下面是一示例:
1 int corePoolSize = Runtime.getRuntime().availableProcessors(); 2 threadsPool = new ThreadPoolExecutor(corePoolSize, corePoolSize, 10l, TimeUnit.SECONDS, 3 new LinkedBlockingQueue(2000));
2.8 Future和FutureTask
Future是一个接口,FutureTask是一个具体实现类。这里先通过两个场景看看其处理方式及优点。
场景1,
现在通过调用一个方法从远程获取一些计算结果,假设有这样一个方法:
1 HashMap data = getDataFromRemote();
如果是最传统的同步方式的使用,我们将一直等待getDataFromRemote()的返回,然后才能继续后面的工作。这个函数是从远程获取数据的计算结果的,如果需要的时间很长,并且后面的那部分代码与这些数据没有关系的话,阻塞在这里等待结果就会比较浪费时间。如何改进呢?
能够想到的办法就是调用函数后马上返回,然后继续向下执行,等需要用数据时再来用或者再来等待这个数据。具体实现有两种方式:一个是用Future,另一个使用回调。
Future的用法
1 Future future = getDataFromRemote2(); 2 //do something 3 HashMap data = future.get();
可以看到,我们调用的方法返回一个Future对象,然后接着进行自己的处理,后面通过future.get()来获取真正的返回值。也即,在调用了getDataFromRemote2后,就已经启动了对远程计算结果的获取,同时自己的线程还在继续处理,直到需要时再获取数据。来看一下getDataFromRemote2的实现:
1 privete Future getDataFromRemote2(){ 2 return threadPool.submit(new Callable(){ 3 public HashMap call() throws Exception{ 4 return getDataFromRemote(); 5 } 6 }); 7 }
可以看到,在getDataFromRemote2中还是使用了getDataFromRemote来完成具体操作,并且用到了线程池:把任务加入到线程池中,把Future对象返回出去。我们调用了getDataFromRemote2的线程,然后返回来继续下面的执行,而背后是另外的线程在进行远程调用及等待的工作。get方法也可设置超时时间参数,而不是一直等下去。
场景2,
key-value的形式存储连接,若key存在则获取,若不存在这个key,则创建新连接并存储。
传统的方式会使用HashMap来存储并判断key是否存在而实现连接的管理。而这在高并发的时候会出现多次创建连接的现象。那么新的处理方式又是怎样呢?
通过ConcurrentHashMap及FutureTask实现高并发情况的正确性,ConcurrentHashMap的分段锁存储满足数据的安全性又不影响性能,FutureTask的run方法调用Sync.innerRun方法只会执行Runnable的run方法一次(即使是高并发情况)。
2.9 并发容器
在JDK中,有一些线程不安全的容器,也有一些线程安全的容器。并发容器是线程安全容器的一种,但是并发容器强调的是容器的并发性,也就是说不仅追求线程安全,还要考虑并发性,提升在容器并发环境下的性能。
加锁互斥的方式确实能够方便地完成线程安全,不过代价是降低了并发性,或者说是串行了。而并发容器的思路是尽量不用锁,比较有代表性的是以CopyOnWrite和Concurrent开头的几个容器。CopyOnWrite容器的思路是在更改容器的时候,把容器写一份进行修改,保证正在读的线程不受影响,这种方式用在读多写少的场景中会非常好,因为实质上是在写的时候重建了一次容器。而以Concurrent开头的容器的具体实现方式则不完全相同,总体来说是尽量保证读不加锁,并且修改时不影响读,所以达到比使用读写锁更高的并发性能。比如上面所说的ConcurrentHashMap,其他的并发容器的具体实现,可直接分析JDK中的源码。