只有在多线程对共享数据进行写操作时,才会有并发错误。
从底层看,有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);
要保证线程安全,就需要保证原子操作,下面来看一个列子:
- package thread;
- import java.util.concurrent.atomic.AtomicReference;
- public class ConcurrentStack<T> {
- private AtomicReference<Node<T>> stacks = new AtomicReference<Node<T>>();
- public T push(T e) {
- Node<T> oldNode, newNode;
- for (;;) { // 这里的处理非常的特别,也是必须如此的。
- oldNode = stacks.get();
- newNode = new Node<T>(e, oldNode);
- if (stacks.compareAndSet(oldNode, newNode)) {
- return e;
- }
- }
- }
- public T pop() {
- Node<T> oldNode, newNode;
- for (;;) {
- oldNode = stacks.get();
- newNode = oldNode.next;
- if (stacks.compareAndSet(oldNode, newNode)) {
- return oldNode.object;
- }
- }
- }
- private static final class Node<T> {
- private T object;
- private Node<T> next;
- private Node(T object, Node<T> next) {
- this.object = object;
- this.next = next;
- }
- }
- }
第二组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。