java concurrent库

只有在多线程对共享数据进行写操作时,才会有并发错误。

 

从底层看,有2个层面:

1. jvm的每个thread的私有stack中的object引用可能共享heap堆中object类。

2. 多cpu中的私有cache数据可能指向公共的RAM中的同一个地址。

 

从现象看,有2个现象:

1. visibility 一个线程写,多个线程读,如果cpu将读数据保存在cache中,写的数据将不能及时更新,volatile 关键词可以让缓存失效。

2. race condition 多个线程同时写, 如果都先取,再加,那么sum结果不是所有到累加值,而是后一个写线程的值覆盖前一个线程的值。 synchronized 关键词可以保证只有一个线程执行当前操作。

synchronized 关键词是锁对象实例的,而不是锁类的。一个类的2个不同对象实例的synchronized的块可以并发。

注意:static方法是在Class实例中,一个类在jvm中只有一个Class实例。

 

关于锁的种类,大家可以参考

http://ifeve.com/java_lock_see/

 

下面进入rt.jar源码

 

首先要介绍的是,

---------------java.concurrent.atomic包,

再学习这个包前,请大家搜索掌握,CAS,unsafe俩个基本知识。

这个包下,第一组AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference这四种基本类,

是提供了cpu一个叫CAS(compare and swap)的原子操作。

看源码,是调用了 unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
要保证线程安全,就需要保证原子操作,下面来看一个列子:

  1. package thread;  
  2. import java.util.concurrent.atomic.AtomicReference;  
  3. public class ConcurrentStack<T> {  
  4.     private AtomicReference<Node<T>>    stacks  = new AtomicReference<Node<T>>();  
  5.     public T push(T e) {  
  6.         Node<T> oldNode, newNode;  
  7.         for (;;) { // 这里的处理非常的特别,也是必须如此的。  
  8.             oldNode = stacks.get();  
  9.             newNode = new Node<T>(e, oldNode);  
  10.             if (stacks.compareAndSet(oldNode, newNode)) {  
  11.                 return e;  
  12.             }  
  13.         }  
  14.     }     
  15.     public T pop() {  
  16.         Node<T> oldNode, newNode;  
  17.         for (;;) {  
  18.             oldNode = stacks.get();  
  19.             newNode = oldNode.next;  
  20.             if (stacks.compareAndSet(oldNode, newNode)) {  
  21.                 return oldNode.object;  
  22.             }  
  23.         }  
  24.     }     
  25.     private static final class Node<T> {  
  26.         private T       object;       
  27.         private Node<T>   next;         
  28.         private Node(T object, Node<T> next) {  
  29.             this.object = object;  
  30.             this.next = next;  
  31.         }  
  32.     }     
  33. }  

第二组AtomicIntegerArray,AtomicLongArray还有AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。

第三组AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater基于反射的实用工具,可以对指定类的指定 volatile 字段进行原子更新。

本质上,array的第几个元素,和object的第几个field都是利用了unsafe来根据类的起始地址,和要更新元素的位偏移量来cas内存。

最后介绍, Striped64 和 LongAdder。

这2个类是对于竞争激烈的求和运算情况下,使用了hash数组的思想。

如果很多进程同时计数sum。 我们可以让sum = a+b.让一些进程hash到竞争a, 其他hash到竞争b。 同理,可以设置更长的数组, 让sum等于数组之和。


---------------java.concurrent.lock包,

先看下lock interface 里的几个方法的区别:

1)lock(), 拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
2)tryLock(),马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。
带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。

3)lockInterruptibly() 调用后一直阻塞到获得锁 但是接受中断信号.

4) newCondition() 通过condiction的wait和signal方法,wait方法释放锁,同时挂起自己等待别的signal方法唤醒自己重新竞争锁。

 

在看下工具类 LockSupport : 提供了unsafe的park,和unpark方法,unpark释放许可,park请求许可.

 

下面讲lock包中最基础的就是AbstractQueuedSynchronizer (AQS) 很多lock实现都继承了这个类。 这个类实现了一个CLH队列,所谓CLH队列,就是一个FIFO的双向列表,

如果新的thread获取lock失败,就会放入CLH表tail,并且park自己。如果当前thread释放自己,就会unpark自己在CLH表的next节点。具体的tryAquire和tryRelease都需要子类实现。

AQS分exclusive和shared俩种模式,可以让单个或多个线程获取锁。



下面开始看各个具体lock实现类。

首先是 reentrantLock: 可重入锁,使用exclusive模式,,如果lock的thread等于当前锁的ownerThread,则state+1.

reentrantLock里有个静态内部类 Sync extends AbstractQueuedSynchronizer, 这个sync 分fair的和unfair的2个实现,fair的需要用AQS的队列保存thread.

ReadWriteLock, 读写锁,内部有读锁和写锁2个锁,

由于读操作没有不一致,读锁是一个没有数量限制的share锁,写操作是个exclusive锁。

StampedLock, jdk8新加读写锁,具有乐观读模式,比reentrantWriteReadLock性能好。

 

ConcurrentHashMap:

在JDK8 中,ConcurrentHashMap实现机制较JDK7发生了很大变化,其摒弃了分段锁(Segment)的概念,而是利用CAS算法,与 Hashtable一样,内部由“数组+链表+红黑树”的方式实现。同时又增加了许多辅助类,例如TreeBin,以实现并发性。

主要设计思想有:

1.和hashMap一样,是node链表的数组,table[],会扩容,链表大于8会转换成红黑树,不同的是,在put操作时,只会synchronize当前桶的一个链表,而不是整个数组,size会用striped64来add。


Semaphore 信号量,有多余一个资源的共享锁。

 

CountDownLatch,

可以用来在一个线程中等待多个线程完成任务的类; 是一个共享锁,

通常的使用场景是,某个主线程接到一个任务,起了n个子线程去完成,但是主线程需要等待这n个子线程都完成任务了以后才开始执行某个操作;

 

CyclicBarrier 多个任务同时等待大家一起完成。

 

ThreadLocal

当static或singleton中的变量,需要每个线程都保存一个私有的副本时,可以使用ThreadLocal。其实现是一个已thread为key的map。

 

 

 

最后介绍下 ThreadPoolExecutor,参数如下:

  • corePoolSize:指线程池的核心大小。每当向线程池内提交新的任务,就会检查当前池内运行的线程的数量。如果小于该值,就会创建新的线程;如果大于该值,则会将任务暂时安排进阻塞队列。
  • maximumPoolSize:指线程池的最大容量。设置该值的意义在于:当向池内添加新的任务,如果当前池内运行的线程数大于corePoolSize且小于maximumPoolSize;并且阻塞队列已经排满,则创建新的线程。
  • keepAliveTime:指的是空闲线程的存活时间。但一定注意该存活时间只针对于那些超过corePoolSize的线程生效。也就是说,如果池内当前即使存在空闲的线程,但如果数量在corePoolSize范围之内。该值则不会生效。
  • unit:很简单,就是设置keepAliveTime的时间单位(如:天、小时、分、秒等)
  • workQueue:阻塞队列,用来存储等待执行的任务。一般来说,这里的阻塞队列有以下几种选择:1.ArrayBlockingQueue; 2.LinkedBlockingQueue; 3.SynchronousQueue;
  • threadFactory:线程创建工厂,顾名思义,主要负责线程的创建工作。
  • RejectedExecutionHandler:拒绝任务时的处理策略。拒绝任务是指:向池内新添加任务时,池内运行的线程数量已经大于或等于maximumPoolSize(即最大容量),那么当前池则会拒绝此次添加的任务。

 

底层实现就是一个AQS,

1. workers
    workers是HashSet<Work>类型,即它是一个Worker集合。而一个Worker对应一个线程,也就是说线程池通过 workers包含了"一个线程集合"。当Worker对应的线程池启动时,它会执行线程池中的任务;当执行完一个任务后,它会从线程池的阻塞队列中取出 一个阻塞的任务来继续运行。
    wokers的作用是,线程池通过它实现了"允许多个线程同时运行"。

2. workQueue
    workQueue是BlockingQueue类型,即它是一个阻塞队列。当线程池中的线程数超过它的容量的时候,线程会进入阻塞队列进行阻塞等待。
    通过workQueue,线程池实现了阻塞功能。

3. mainLock
    mainLock是互斥锁,通过mainLock实现了对线程池的互斥访问。

 

workers是一个shared模式的可用资源池, workQueue是需要资源的thread队列,mainLock实现了shared模式的锁对共享资源进行synchronize的存取操作。

 

计算密集型,就是应用需要非常多的CPU计算资源,在多核CPU时代,我们要让每一个CPU核心都参与计算,避免过多的线程上下文切换,比较理想方案是:计算密集型的较理想线程数 = CPU内核线程数+1。

 

对于IO密集型的应用,读 写数据库,读写文件都涉及到IO,一旦发生IO,线程就会处于等待状态,当IO结束,数据准备好后,线程才会继续执行。因此从这里可以发现,对于IO密集 型的应用,我们可以多设置一些线程池中线程的数量,这样就能让在等待IO的这段时间内,线程可以去做其它事,提高并发处理效率。比较合适的方案是:IO密 集型应用比较理想线程数= CPU内核线程数*2+1。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值