当前位置 : 首页 » 文章分类 :  开发  »  Java面试准备-(03)线程和并发

Java面试准备-(03)线程和并发

Java面试准备笔记


java 多线程与并发(concurrent包)

随笔分类 - Java并发编程 - 海 子
http://www.cnblogs.com/dolphin0520/category/602384.html

随笔分类 - Java多线程
http://www.cnblogs.com/xiaoxi/category/961349.html

方腾飞 - 聊聊并发
http://www.infoq.com/cn/profile/方腾飞

专栏-Java并发编程系列
http://blog.csdn.net/column/details/concurrency.html

ConcurrentHashMap

ConcurrentHashMap是一个线程安全的Hash Table,它的主要功能是提供了一组和HashTable功能相同但是线程安全的方法。ConcurrentHashMap可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,不用对整个ConcurrentHashMap加锁。

ConcurrentHashMap内部实现(jdk1.7)

ConcurrentHashMap是使用了锁分段技术技术来保证线程安全的,锁分段技术:首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

相对 HashMap 和 Hashtable, ConcurrentHashMap 增加了Segment 层,每个Segment 原理上等同于一个 Hashtable, ConcurrentHashMap 为 Segment 的数组。

final Segment<K,V> segmentFor(int hash) {
        return segments[(hash >>> segmentShift) & segmentMask];
    }

public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);
    }

public V get(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).get(key, hash);
    }

向 ConcurrentHashMap 中插入数据或者读取数据,首先都要讲相应的 Key 映射到对应的 Segment,因此不用锁定整个类, 只要对单个的 Segment 操作进行上锁操作就可以了。理论上如果有 n 个 Segment,那么最多可以同时支持 n 个线程的并发访问,从而大大提高了并发访问的效率。另外 rehash() 操作也是对单个的 Segment 进行的,所以由 Map 中的数据量增加导致的 rehash 的成本也是比较低的。

ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此,这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长,但是带来的好处是写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上),所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。

单个 Segment 的进行数据操作的源码如下:

V put(K key, int hash, V value, boolean onlyIfAbsent) {
            lock();
            try {
                int c = count;
                if (c++ > threshold) // ensure capacity
                    rehash();

                …… // 代码省略,具体请查看源码

            } finally {
                unlock();
            }
        }

V replace(K key, int hash, V newValue) {
            lock();
            try {
                HashEntry<K,V> e = getFirst(hash);

                …… // 代码省略,具体请查看源码

            } finally {
                unlock();
            }
        }

可见对 单个的 Segment 进行的数据更新操作都是 加锁的,从而能够保证线程的安全性。

ConcurrentHashMap的应用场景是高并发,但是并不能保证线程安全,而同步的HashMap和HashMap的是锁住整个容器,而加锁之后ConcurrentHashMap不需要锁住整个容器,只需要锁住对应的Segment就好了,所以可以保证高并发同步访问,提升了效率。

Java - 线程安全的 HashMap 实现方法及原理
http://liqianglv2005.iteye.com/blog/2025016

Java并发编程之ConcurrentHashMap
http://www.iteye.com/topic/1103980

put和remove操作(只能链表头部插入)

jdk1.7中的HashEntry结构:

static final class HashEntry<K,V> {
    final K key;
    final int hash;
    volatile V value;
    final HashEntry<K,V> next;
}

可以看到除了value不是final的,其它值都是final的,这意味着不能从hash链的中间或尾部添加或删除节点,因为这需要修改next引用值,所有的节点的修改只能从头部开始。对于put操作,可以一律添加到Hash链的头部。但是对于remove操作,可能需要从中间删除一个节点,这就需要将要删除节点的前面所有节点整个复制一遍,最后一个节点指向要删除结点的下一个结点。

注:jdk1.8中使用Node结构

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

    transient volatile Node<K,V>[] table; //存储键值对的Node数组(桶),默认长度16

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
    }
}

ConcurrentHashMap的get为什么可以不加锁?

ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。如果使用传统的技术,如HashMap中的实现,如果允许可以在hash链的中间添加或删除元素,读操作不加锁将得到不一致的数据。ConcurrentHashMap实现技术是保证HashEntry几乎是不可变的。HashEntry代表每个hash链中的一个节点,其结构如下所示:

jdk1.7之前的get方法:

public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}

V get(Object key, int hash) {
        if (count != 0) { // read-volatile // ①
            HashEntry<K,V> e = getFirst(hash);
            while (e != null) {
                if (e.hash == hash && key.equals(e.key)) {
                    V v = e.value;
                    if (v != null)  // ② 注意这里
                        return v;
                    return readValueUnderLock(e); // recheck
                }
                e = e.next;
            }
        }
        return null;
}

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读,我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?
原因是它的get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value。定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

为什么读取到的结点value有可能是空的?

理论上结点的值不可能为空,这是因为put的时候就进行了判断,如果为空就要抛NullPointerException。
如果另一个线程刚好new 这个对象时,当前线程来get它。因为没有同步,就可能会出现当前线程得到的newEntry对象是一个没有完全构造好的对象引用。没有锁同步的话,new 一个对象对于多线程看到这个对象的状态是没有保障的,这里同样有可能一个线程new这个对象的时候还没有执行完构造函数就被另一个线程得到这个对象引用。
所以才需要判断一下:if (v != null) 如果确实是一个不完整的对象,则使用锁的方式再次get一次。

但是,get方法只能保证读取到几乎最新的数据,虽然可能不是最新的。要得到最新的数据,只有采用完全的同步。

聊聊并发(四)——深入分析ConcurrentHashMap(对为什么get方法不需要加锁解释的很简洁)
http://www.infoq.com/cn/articles/ConcurrentHashMap

ConcurrentHashMap(详细讲了happens-before,解释为什么get可以不加锁,但太详细太繁琐了)
http://www.cnblogs.com/yydcdut/p/3959815.html

Java并发编程之ConcurrentHashMap
http://www.iteye.com/topic/1103980

ConcurrentHashMap之实现细节
http://www.iteye.com/topic/344876

ConcurrentHashMap不能保证完全线程安全

ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。

ConcurrentHashMap是线程安全的,那是在他们的内部操作,其外部操作还是需要自己来保证其同步的

ConcurrentHashMap、synchronized与线程安全
http://blog.csdn.net/sadfishsc/article/details/42394955

ConcurrentHashMap并不是绝对线程安全的
http://blog.51cto.com/laokaddk/1345191

java8对ConcurrentHashMap的改进

改进一:不再使用segments分段加锁(Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。),直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。

改进二:将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构。
对于hash表来说,最核心的能力在于将key hash之后能均匀的分布在数组中。如果hash之后散列的很均匀,那么table数组中的每个队列长度主要为0或者1。但实际情况并非总是如此理想,虽然ConcurrentHashMap类默认的加载因子为0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式,那么查询某个节点的时间复杂度为O(n);因此,对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。

java8中对ConcurrentHashMap的改进
http://blog.csdn.net/wangxiaotongfan/article/details/52074160

Unsafe与CAS

在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。因为当前线程中的值已经不是最新的值,你的修改很可能会覆盖掉其他线程修改的结果。这一点与乐观锁,SVN的思想是比较类似的。

ConcurrentHashMap定义了三个原子操作,用于对指定位置的节点进行操作。正是这些原子操作保证了ConcurrentHashMap的线程安全。

//获得在i位置上的Node节点
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

//利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少
//在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改
//因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

//利用volatile方法设置节点位置的值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

ConcurrentHashMap总结
https://my.oschina.net/hosee/blog/675884


CopyOnWriteArrayList

CopyOnWriteArrayList是ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。

从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList的add方法如下:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。

CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景

使用CopyOnWriteMap需要注意两件事情:

  1. 减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
  2. 使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。

缺点:
1、内存占有问题:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。
2、数据一致性:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器

聊聊并发-Java中的Copy-On-Write容器
http://ifeve.com/java-copy-on-write/

线程安全的CopyOnWriteArrayList介绍
http://blog.csdn.net/linsongbin1/article/details/54581787

CopyOnWriteArrayList与Collections.synchronizedList对比

CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。
1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。
2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。

  • CopyOnWriteArrayList在线程对其进行变更操作的时候,会拷贝一个新的数组以存放新的字段,因此写操作性能很差;
  • 而Collections.synchronizedList读操作采用了synchronized,因此读性能较差。

java8中对ConcurrentHashMap的改进
http://blog.csdn.net/wangxiaotongfan/article/details/52074160


创建并使用线程Thread

java线程的5种状态及转换

线程从创建到最终的消亡,要经历若干个状态。一般来说,线程包括以下这几个状态:

新建(NEW)

新创建了一个线程对象。
当需要新起一个线程来执行某个子任务时,就创建了一个线程。但是线程创建之后,不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。

可运行(RUNNABLE)或就绪

线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。

运行(RUNNING)

可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。

阻塞(BLOCKED)

阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。

阻塞的情况分三种:

等待阻塞(wait)

运行(running)的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中。

同步阻塞(等待synchronized,Lock)

运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。

其他阻塞(sleep等)

运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。

死亡(DEAD)

线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

java线程状态装换图


java线程状态装换图

Java线程的5种状态及切换(透彻讲解)
http://blog.csdn.net/pange1991/article/details/53860651


Thread类常用方法

Java并发编程:Thread类的使用 - 海子
http://www.cnblogs.com/dolphin0520/p/3920357.html

Java线程的5种状态及切换(透彻讲解)
http://blog.csdn.net/pange1991/article/details/53860651

sleep睡眠阻塞不释放锁

sleep(long millis)
Thread.sleep(long millis),一定是当前线程调用此方法,当前线程进入阻塞,但不释放对象锁,millis后线程自动苏醒进入可运行状态。作用:给其它线程执行机会的最佳方式。

yield交出时间片不阻塞不释放锁

Thread.yield(),一定是当前线程调用此方法,当前线程放弃获取的cpu时间片,由运行状态变会可运行状态,让OS再次选择线程。作用:让相同优先级的线程轮流执行,但并不保证一定会轮流执行。实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。Thread.yield()不会导致阻塞。

调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

join阻塞其他线程

t.join()/t.join(long millis),当前线程里调用其它线程t的join方法,当前线程阻塞,但不释放对象锁,直到线程1执行完毕或者millis时间到,当前线程进入可运行状态。

假如在main线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕,如果调用的是指定了时间参数的join方法,则等待一定的事件。

实际上调用join方法是调用了Object的wait方法,这个可以通过查看源码得知。
wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。由于wait方法会让线程释放对象锁,所以join方法同样会让线程释放对一个对象持有的锁。

join方法会使得被阻塞线程释放对象锁吗

有一个疑问,join方法会使得被阻塞线程释放对象锁吗?
join 是一个同步方法,调用join的时候会获取调用主体的对象锁,即t1.join() 会先获取t1对象锁,然后join内部进行wait释放锁。
1.如果外部线程已经持有了非t1的锁,调用join是不会释放非t1锁的
2.如果外部线程恰好持有了t1对象锁,那么调用t1.join会释放,此时外部线程不在持有t1对象锁。

wait阻塞释放锁

obj.wait(),当前线程调用对象的wait()方法,当前线程释放对象锁,进入等待队列。依靠notify()/notifyAll()唤醒或者wait(long timeout)timeout时间到自动唤醒。

wait方法会让线程进入阻塞状态,并且会释放线程占有的锁,并交出CPU执行权限。

notify唤醒1个等待此对象的线程

obj.notify()唤醒在此对象监视器上等待的单个线程,选择是任意性的。notifyAll()唤醒在此对象监视器上等待的所有线程。
直接调用interrupt方法不能中断正在运行中的线程。

interrupt中断

interrupt()这个方法通过修改被调用线程的中断状态来告知那个线程,说它被中断了。

  • 对于非阻塞中的线程, 只是改变了中断状态, 即Thread.isInterrupted()将返回true;
  • 对于可取消的阻塞状态中的线程, 比如等待在Thread.sleep(), Object.wait(), Thread.join()这些函数上的线程,这个线程收到中断信号后, 会抛出InterruptedException, 同时会把中断状态置回为true
中断处于阻塞状态的线程

利用interrupt方法对于阻塞中的线程会抛出InterruptedException异常的特点来中断处于阻塞状态的线程:

@Override
public void run() {
    try {
        while (true) {
            // 执行任务...
        }
    } catch (InterruptedException ie) {
        // 由于产生InterruptedException异常,退出while(true)循环,线程终止!
    }
}

此时,外部调用线程的interrupt()即可中断阻塞中的线程。

中断处于运行状态的线程

注意:直接调用interrupt方法不能中断正在运行中的线程,因为interrupt()只是修改线程的中断状态标志位,并不会使线程停止。

通常,我们通过“标记”方式终止处于“运行状态”的线程。
方法一、通过“中断标记”终止线程。

public class Thread3 extends Thread{
    public void run(){
        while(true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println("Someone interrupted me.");
                break;
            }
            else{
                System.out.println("Thread is Going...");
                //do something
            }
        }
    }
}

方法二、通过自定义标记终止线程

private volatile boolean flag= true;
protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务...
    }
}

理解java线程的中断(interrupt)
https://blog.csdn.net/canot/article/details/51087772

Java多线程系列–“基础篇”09之 interrupt()和线程终止方式
https://www.cnblogs.com/skywang12345/p/3479949.html


线程优先级与守护线程

java中线程的优先级

Thread类中有个成员priority,表示线程的优先级,最大值为10,最小值为1,默认值为5,值越大优先级越高
private int priority;

每个线程都有一个优先级。“高优先级线程”会优先于“低优先级线程”执行。
创建新的子线程时,子线程的优先级被设置为等于“创建它的主线程的优先级”

线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的并非没机会执行。

优先级高的线程不一定先执行

JAVA中设置了线程优先级,能保证这个线程一定先执行吗?
其实,即使设置了线程的优先级,一样无法确保这个线程一定先执行,因为它有很大的随机性。它并无法控制执行哪个线程,因为线程的执行,是抢占资源后才能执行的操作,而抢点资源时,最多是给于线程优先级较高的线程一点机会而已,能不能抓住可是不一定的。。
说到底就一句话:线程优化级较高的线程不一定先执行。

守护线程与非守护线程的区别

Java的线程分为两种:User Thread(用户线程)、DaemonThread(守护线程)。

只要当前JVM实例中尚存任何一个非守护线程没有结束,守护线程就全部工作;当最后一个非守护线程结束时,守护线程随着JVM一同结束工作,Daemon作用是为其他线程提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),他就是一个很称职的守护者。

User和Daemon两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果 User Thread已经全部退出运行了,只剩下Daemon Thread存在了,虚拟机也就退出了。 因为没有了被守护者,Daemon也就没有工作可做了,也就没有继续运行程序的必要了。

优先级:
守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。

将线程转换为守护线程可以通过调用Thread对象的setDaemon(true)方法来实现。在使用守护线程时需要注意一下几点:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

守护线程有一个应用场景,就是当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。不过博主推荐,如果真有这种场景,还是用中断的方式实现比较合理。

Java 守护线程概述
http://www.importnew.com/26834.html

Java的守护线程与非守护线程
https://www.cnblogs.com/lixuan1998/p/6937986.html

区别守护进程与守护线程

先把 守护线程 与 守护进程 这二个极其相似的说法区分开,
守护进程通常是为了防止某些应用因各种意外原因退出,而在后台独立运行的系统服务或应用程序。 比如:我们开发了一个邮件发送程序,一直不停的监视队列池,发现有待发送的邮件,就将其发送出去。如果这个程序挂了(或被人误操作关了),邮件就不发出去了,为了防止这种情况,再开发一个类似windows 系统服务的应用,常驻后台,监制这个邮件发送程序是否在运行,如果没运行,则自动将其启动。

而我们今天说的java中的守护线程(Daemon Thread) 指的是一类特殊的Thread,其优先级特别低(低到甚至可以被JVM自动终止),通常这类线程用于在空闲时做一些资源清理类的工作,比如GC线程,如果JVM中所有非守护线程(即:常规的用户线程)都结束了,守护线程会被JVM中止,想想其实也挺合理,没有任何用户线程了,自然也不会有垃圾对象产生,GC线程也没必要存在了。

java并发编程学习: 守护线程(Daemon Thread)
https://www.cnblogs.com/yjmyzz/p/daemon-thread-demo.html


java中创建线程的三种方法

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用三种方式来创建线程,如下所示:
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程

java中创建线程的三种方法以及区别
https://www.cnblogs.com/3s540/p/7172146.html

继承Thread类,重写run方法

通过继承Thread类来创建并启动多线程的一般步骤如下:
1、定义Thread类的子类,并重写该类的run()方法,该方法的方法体就是线程需要完成的任务,run()方法也称为线程执行体。
2、创建Thread子类的实例,也就是创建了线程对象
3、启动线程,即调用线程的start()方法

public class MyThread extends Thread{//继承Thread类
  public void run(){
  //重写run方法
  }
}

public class Main {
  public static void main(String[] args){
    new MyThread().start();//创建并启动线程
  }
}

或者使用匿名类,写起来更简洁:

public class Main {
  public static void main(String[] args){
        new Thread(){
            @Override
            public void run() {
                //重写run方法
            };
        }.start();
  }
}

实现Runnable接口,放入Thread类中

通过实现Runnable接口创建并启动线程一般步骤如下:
1、定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体
2、创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象
3、第三部依然是通过调用线程对象的start()方法来启动线程

public class MyThread2 implements Runnable {//实现Runnable接口
  public void run(){
  //重写run方法
  }
}

public class Main {
  public static void main(String[] args){
    //创建并启动线程
    MyThread2 myThread=new MyThread2();
    Thread thread=new Thread(myThread);
    thread().start();
    //或者    new Thread(new MyThread2()).start();
  }
}

或者直接使用匿名内部类,写起来更简洁:

public class Main {
  public static void main(String[] args){
        new Thread(new Runnable(){
                @Override
                public void run(){
                    //重写run方法
                }
            }
        ).start();
  }
}
为什么不能直接调用Runnable接口的run()方法运行?

常见错误:调用run()方法而非start()方法
创建并运行一个线程所犯的常见错误是调用线程的run()方法而非start()方法,如下所示:

Thread newThread = new Thread(MyRunnable());
newThread.run();  //should be start();

起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。

run方法中只是定义需要执行的任务,如果直接调用run方法,即相当于在主线程中执行run方法,跟普通的方法调用没有任何区别,此时并不会创建一个新的线程来执行定义的任务。

使用Callable和Future

使用Callable和Future创建并启动有返回值的线程有两种方法
一、利用FutureTask封装Callable再由Thread启动,详细步骤如下:
1、创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值
3、使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

二、利用ExecutorService的submit提交Callable任务到线程池执行
详见Callable接口的使用

三种创建线程方法对比

实现Runnable和实现Callable接口的方式基本相同,只不过是后者是执行call()方法有返回值,前者是执行体run()方法无返回值,因此可以把这两种方式归为一种这种方式与继承Thread类的方法之间的差别如下:
1、线程只是实现Runnable或实现Callable接口,还可以继承其他类。
2、这种方式下,多个线程可以共享一个target对象,非常适合多线程处理同一份资源的情形。
3、但是编程稍微复杂,如果需要访问当前线程,必须调用Thread.currentThread()方法。
4、继承Thread类的线程类不能再继承其他父类(Java单继承决定)。

注:一般推荐采用实现接口的方式来创建多线程

java中创建线程的三种方法以及区别
https://www.cnblogs.com/3s540/p/7172146.html


线程工厂

创建Thread需要哪些参数

Thread类参数最全的构造方法如下,需要四个参数:
public Thread(ThreadGroup group, Runnable target, String name, long stackSize)

ThreadGroup group:线程组。如果 group 为 null,并且有安全管理器,则该组由安全管理器的 getThreadGroup 方法确定。如果 group 为 null,并且没有安全管理器,或安全管理器的 getThreadGroup 方法返回 null,则该组与创建新线程的线程被设定为相同的 ThreadGroup。

Runnable target:线程的执行体。如果 target 参数不是 null,则 target 的 run 方法在启动该线程时调用。如果 target 参数为 null,则该线程的 run 方法在该线程启动时调用。

String name:新线程的名称。

long stackSize:新线程的预期堆栈大小,为零时表示忽略该参数。

新创建线程的优先级被设定为创建该线程的线程的优先级,即当前正在运行的线程的优先级。方法 setPriority 可用于将优先级更改为一个新值。
最大值为10,最小值为1,默认值为5

当且仅当创建新线程的线程当前被标记为守护线程时,新创建的线程才被标记为守护线程。方法 setDaemon 可用于改变线程是否为守护线程。

使用线程工厂创建线程有什么好处?

为什么要使用线程工厂呢?
其实就是为了统一在创建线程时设置一些参数,如是否守护线程。设置线程的一些特性等,如优先级、线程组、线程名称(比如我们想使用统一的名称前缀)、线程栈大小。以及还能增加一些线程统计特性。

ThreadFactory

java提供了一个ThreadFactory接口,他只有一个方法,就是创建一个线程

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

我们可以继承ThreadFactory接口来实现自己的线程工厂

011-ThreadFactory线程工厂
https://www.cnblogs.com/bjlhx/p/7609100.html

Executors的默认线程工厂

Java并发API的一些高级工具,如执行者框架(Executor framework)或Fork/Join框架(Fork/Join framework),都使用线程工厂创建线程。
比如ThreadPoolExecutor类就有个参数是ThreadFactory,我们可以传入自己定义的线程工厂。

下面来看看Executors类的默认线程工厂, 我们创建线程池时使用的就是这个线程工厂

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);//原子类,线程池编号
    private final ThreadGroup group;//线程组
    private final AtomicInteger threadNumber = new AtomicInteger(1);//线程数目
    private final String namePrefix;//为每个创建的线程添加的前缀

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();//取得线程组
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                    "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);//真正创建线程的地方,设置了线程的线程组及线程名
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)//默认是正常优先级
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

在上面的代码中,可以看到线程池中默认的线程工厂实现是很简单的,它做的事就是统一给线程池中的线程设置线程group、统一的线程前缀名。以及统一的优先级。

011-ThreadFactory线程工厂
https://www.cnblogs.com/bjlhx/p/7609100.html


Callable

Callable接口实际上是属于Executor框架中的功能类,Callable接口与Runnable接口的功能类似,但提供了比Runnable更加强大的功能:

  • Callable可以在任务结束的时候提供一个返回值,Runnable无法提供这个功能
  • Callable的call方法分可以抛出异常,而Runnable的run方法不能抛出异常。

Callable接口的实例被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值

利用FutureTask封装Callable再由Thread启动

下面来看一个简单的例子:

public class CallableAndFuture {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

利用ExecutorService的submit提交Callable任务到线程池执行

下面来看另一种方式使用Callable和Future,通过ExecutorService线程池的submit方法执行Callable,并返回Future,代码如下:

public class CallableAndFuture {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        Future<Integer> future = threadPool.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        });
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Java线程(七):Callable和Future
http://blog.csdn.net/ghsau/article/details/7451464

java并发编程–Runnable Callable及Future
https://www.cnblogs.com/MOBIN/p/6185387.html

Runnable接口和Callable接口的异同点

相同点:
1、两者都是多线程的任务接口;
2、两者都需要调用Thread.start()启动线程;
不同点:
1、Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的。
2、Callable规定的方法是call(),Runnable规定的方法是run()。
3、Callable的任务执行后可返回值,而Runnable的任务是不能返回值(是void)。
4、call方法可以抛出异常,run方法不可以。
5、提交到线程池运行的方式不同,Runnable使用ExecutorService的execute方法,Callable使用submit方法。

Java并发编程:Callable、Future和FutureTask
http://www.cnblogs.com/xiaoxi/p/8303574.html


Future

java5提供了Future接口用以保存异步计算的结果,可以在我们执行任务时去做其他工作。

Future用于对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果

在Future接口里定义了几个公共方法来控制它关联的Callable任务:

  • V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。如果计算被取消了则抛出异常
  • V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回则抛出异常
  • cancel(boolean mayInterruptIfRunning):试图取消执行的任务,参数为true时直接中断正在执行的任务,否则直到当前任务执行完成,成功取消后返回true,否则返回false
  • boolean isDone():若Callable任务完成,返回True
  • boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

FutureTask

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,又实现了Runnable接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

FutureTask类通过实现RunnableFuture接口来同时实现了Runnable和Future接口

public class FutureTask<V> implements RunnableFuture<V>{
...
}

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

java中创建线程的三种方法以及区别
https://www.cnblogs.com/3s540/p/7172146.html

java并发编程–Runnable Callable及Future
https://www.cnblogs.com/MOBIN/p/6185387.html


ThreadLocal

ThreadLocal,即线程本地变量,ThreadLocal变量在每个线程中都有一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

为什么要使用ThreadLocal变量?

在并发编程的时候,成员变量如果不做任何处理其实是线程不安全的,各个线程都在操作同一个变量,显然是不行的,并且我们也知道volatile这个关键字也是不能保证线程安全的。那么在有一种情况之下,我们需要满足这样一个条件:变量是同一个,但是每个线程都使用同一个初始值,也就是使用同一个变量的一个新的副本。这种情况之下ThreadLocal就非常使用,比如说DAO的数据库连接,我们知道DAO是单例的,那么他的属性Connection就不是一个线程安全的变量。而我们每个线程都需要使用他,并且各自使用各自的。这种情况,ThreadLocal就比较好的解决了这个问题。

ThreadLocal的四个方法

ThreadLocal类提供了4个操作数据的方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,
set()用来设置当前线程中变量的副本,
remove()用来移除当前线程中变量的副本,
initialValue()返回此线程局部变量的当前线程的“初始值”。

ThreadLocal实现原理

1、每个Thread对象内部都维护了一个ThreadLocalMap这样一个ThreadLocal的Map,可以存放若干个ThreadLocal。

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

2、当我们在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value(注意用的key是当前ThreadLocal对象,即this),否则进行初始化,初始化就是将initialValue的值set到ThreadLocal中。

public T get() {
    Thread t = Thread.currentThread(); //获取当前线程
    ThreadLocalMap map = getMap(t); //获取到当前线程的ThreadLocalMap对象
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //取出ThreadLocal的value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue(); //进行初始化,将initialValue的值set到ThreadLocal中
}

3、set()方法如下,也是先获取当前线程,取出ThreadLocalMap对象,非空则set值,key是当前ThreadLocal对象,value是T类型变量;为空则创建ThreadLocalMap对象后set值

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

从本质来讲,就是每个线程都维护了一个map,而这个map的key就是ThreadLocal对象(每个线程中可以有多个threadLocal变量,对应不同的key),而值就是我们set的那个值,每次线程在get的时候,都从自己的变量中取值,既然从自己的变量中取值,那肯定就不存在线程安全问题。

什么时候使用ThreadLocal?

最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
例如:

//使用匿名内部类,重写initialValue()方法
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        Class.forName("com.mysql.jdbc.Driver");
        return DriverManager.getConnection("url", "userName", "password");
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}

Java并发编程:深入剖析ThreadLocal - 海子
http://www.cnblogs.com/dolphin0520/p/3920407.html

ThreadLocal详解
https://www.cnblogs.com/dreamroute/p/5034726.html


ThreadPoolExecutor自定义线程池

Java线程池的核心类

package java.util.concurrent;

public class ThreadPoolExecutor extends AbstractExecutorService {
  public ThreadPoolExecutor(int corePoolSize,
                            int maximumPoolSize,
                            long keepAliveTime,
                            TimeUnit unit,
                            BlockingQueue<Runnable> workQueue,
                            ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) {
      if (corePoolSize < 0 ||
          maximumPoolSize <= 0 ||
          maximumPoolSize < corePoolSize ||
          keepAliveTime < 0)
          throw new IllegalArgumentException();
      if (workQueue == null || threadFactory == null || handler == null)
          throw new NullPointerException();
      this.acc = System.getSecurityManager() == null ?
              null :
              AccessController.getContext();
      this.corePoolSize = corePoolSize;
      this.maximumPoolSize = maximumPoolSize;
      this.workQueue = workQueue;
      this.keepAliveTime = unit.toNanos(keepAliveTime);
      this.threadFactory = threadFactory;
      this.handler = handler;
  }
}

构造方法参数

int corePoolSize:核心池的大小
核心线程会一直存活,及时没有任务需要执行
当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理
设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭

int maximumPoolSize:线程池最大线程数
当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常

long keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。
TimeUnit unit:参数keepAliveTime的时间单位
当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize
如果allowCoreThreadTimeout=true,则会直到线程数量=0

BlockingQueue<Runnable> workQueue:一个阻塞队列,用来存储等待执行的任务
ThreadFactory threadFactory:线程工厂,主要用来创建线程;

RejectedExecutionHandler handler:表示当拒绝处理任务时的策略
两种情况会拒绝处理任务:
当线程数已经达到maxPoolSize,且队列已满,会拒绝新任务
当线程池被调用shutdown()后,会等待线程池里的任务执行完毕,再shutdown。如果在调用shutdown()和线程池真正shutdown之间提交任务,会拒绝新任务
线程池会调用rejectedExecutionHandler来处理这个任务。如果没有设置默认是AbortPolicy,会抛出异常

其他非构造参数
allowCoreThreadTimeout:允许核心线程超时

JAVA ThreadPoolExecutor线程池参数设置技巧
https://www.imooc.com/article/5887

有哪几种阻塞队列?

workQueue参数可选的队列有:

  • ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  • LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
  • SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  • PriorityBlockingQueue:一个具有优先级得无限阻塞队列。

向线程池提交任务的两种方法(execute,submit)

execute() Runnable任务

我们可以使用execute提交Runnable任务,但是execute方法没有返回值,所以无法判断任务知否被线程池执行成功。

submit() Callable任务

我们也可以使用submit提交Callable任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

void execute(Runnable task)
public <T> Future<T> submit(Callable<T> task)

返回一个表示任务的未决结果的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。
如果想立即阻塞任务的等待,则可以使用 result = exec.submit(aCallable).get(); 形式的构造。

何时创建新线程?(队列与池交互)

ThreadPoolExecutor 将根据 corePoolSize和 maximumPoolSize设置的边界自动调整池大小。当新任务在方法 execute(Runnable) 中提交时,如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的。如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程。如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心和最大池大小仅基于构造来设置,不过也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。

当提交一个新任务到线程池时,线程池的处理流程如下:

  • 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
  • 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
  • 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

线程池按以下行为执行任务

  1. 当线程数小于核心线程数时,创建线程。
  2. 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
  3. 当线程数大于等于核心线程数,且任务队列已满。
    (1)若线程数小于最大线程数,创建线程
    (2)若线程数等于最大线程数,抛出异常,拒绝任务

拒绝任务处理策略(饱和策略)

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。

以下是JDK1.5提供的四种策略。
AbortPolicy:直接抛出异常。默认。
CallerRunsPolicy:只用调用者所在线程来运行任务。
DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
DiscardPolicy:不处理,丢弃掉。但是不抛出异常。

线程池状态(如何关闭线程池?)

在ThreadPoolExecutor中定义了一个volatile变量,另外定义了几个static final变量表示线程池的各个状态:

volatile int runState;
static final int RUNNING    = 0;
static final int SHUTDOWN   = 1;
static final int STOP       = 2;
static final int TERMINATED = 3;

runState表示当前线程池的状态,它是一个volatile变量用来保证线程之间的可见性;
下面的几个static final变量表示runState可能的几个取值。
当创建线程池后,初始时,线程池处于RUNNING状态;
如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。

使用示例

public class Test {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,
                new ArrayBlockingQueue<Runnable>(5));

        for(int i=0;i<15;i++){
            MyTask myTask = new MyTask(i);
            executor.execute(myTask);
            System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
            executor.getQueue().size()+",已执行玩别的任务数目:"+executor.getCompletedTaskCount());
        }
        executor.shutdown();
    }
}

class MyTask implements Runnable {
    private int taskNum;

    public MyTask(int num) {
        this.taskNum = num;
    }

    @Override
    public void run() {
        System.out.println("正在执行task "+taskNum);
        try {
            Thread.currentThread().sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("task "+taskNum+"执行完毕");
    }
}

合理配置线程池的大小

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
任务的优先级:高,中和低。
任务的执行时间:长,中和短。
任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。

  • CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。
  • IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2×Ncpu。
  • 混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。

我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

深入理解Java之线程池
http://www.importnew.com/19011.html

聊聊并发(三)Java线程池的分析和使用
http://ifeve.com/java-threadpool/

多线程和线程池(为什么要用线程池?)

一种是 多线程,一种是线程池。对于多线程模式,也就说来了任务,就会新建一个线程来执行任务
这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。
因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。


Executors自带线程池

在java doc中,并不提倡我们直接使用ThreadPoolExecutor,而是使用Executors类中提供的几个静态方法来创建线程池:

Executors.newCachedThreadPool(); //创建一个缓冲池,缓冲池容量大小为Integer.MAX_VALUE
Executors.newSingleThreadExecutor(); //创建容量为1的缓冲池
Executors.newFixedThreadPool(int); //创建固定容量大小的缓冲池
Executors.newScheduledThreadPool(int); //调度型线程池-这个池子里的线程可以按schedule依次delay执行,或周期执行

下面是这几个静态方法的具体实现:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

从它们的具体实现来看,它们实际上也是调用了ThreadPoolExecutor,只不过参数都已配置好了。
其中调度线程池的构造方法如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

本质上也是调用其父类ThreadPoolExecutor的构造器,只是它使用的工作队列是java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue,通过名字我们都可以猜到这个是一个延时工作队列

  • newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue;
  • newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,也使用的LinkedBlockingQueue;
  • newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

实际中,如果Executors提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置ThreadPoolExecutor的参数有点麻烦,要根据实际任务的类型和数量来进行配置。

深入理解Java之线程池
http://www.importnew.com/19011.html

Java ExecutorService四种线程池的例子与说明
http://blog.csdn.net/nk_tf/article/details/51959276

java常用的几种线程池比较
https://www.cnblogs.com/aaron911/p/6213808.html

Java并发编程(19):并发新特性—Executor框架与线程池(含代码)
http://www.importnew.com/20809.html


并发编程辅助工具

CountDownLatch

CountDownLatch类位于java.util.concurrent包下,利用它可以实现类似计数器的功能。比如有一个任务A,它要等待其他4个任务执行完毕之后才能执行,此时就可以利用CountDownLatch来实现这种功能了。

构造方法:
public CountDownLatch(int count) { }; //参数count为计数值

主要方法:
public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行

public void countDown() { }; //将count值减1

使用示例,主线程等待2个子线程执行完后才继续:

public class Test {
    public static void main(String[] args) {
        final CountDownLatch latch = new CountDownLatch(2);

        //new thread.start,创建并启动一个线程,在run()方法中,执行完任务后latch.countDown()
        //new thread.start,创建并启动一个线程,在run()方法中,执行完任务后latch.countDown()

        latch.await();//主线程挂起,等待latch值减为0后才继续执行
    }
}

Java并发编程:CountDownLatch、CyclicBarrier和Semaphore - 海子
http://www.cnblogs.com/dolphin0520/p/3920397.html

CyclicBarrier

CyclicBarrier字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。

构造方法:
public CyclicBarrier(int parties) 参数parties指让多少个线程或者任务等待至barrier状态
public CyclicBarrier(int parties, Runnable barrierAction) 参数barrierAction为当这些线程都达到barrier状态时会执行的内容。

主要方法:
public int await() 用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
public int await(long timeout, TimeUnit unit) 让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

使用示例,多个线程并发进行写操作,当所有线程写操作完成后,才继续执行后续任务

public class Test {
    public static void main(String[] args) {
        CyclicBarrier barrier  = new CyclicBarrier(2,new Runnable() {
            @Override
            public void run() {
                System.out.println("当前线程"+Thread.currentThread().getName());
            }
        });

        //new thread.start,创建并启动一个线程,run()方法包括三部分:写操作,barrier.await(),后续任务
        //new thread.start,创建并启动一个线程,run()方法包括三部分:写操作,barrier.await(),后续任务
    }
}

启动的多个线程中,先执行完写操作的线程会等待所有线程执行完写操作,当所有线程执行完写操作后(达到barrier状态)后,由最后一个进入barrier的线程去执行barrier的Runnable任务。然后所有线程继续执行后续任务。

Java并发编程:CountDownLatch、CyclicBarrier和Semaphore - 海子
http://www.cnblogs.com/dolphin0520/p/3920397.html

CountDownLatch和CyclicBarrier区别(可重用)

CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

CyclicBarrier原理(Lock,计数器,Generation换代)

实现原理:在CyclicBarrier的内部定义了一个Lock对象,每当一个线程调用CyclicBarrier的await方法时,将剩余拦截的线程数减1,然后判断剩余拦截数是否为0,如果不是,进入Lock对象的条件队列等待。如果是,执行barrierAction对象的Runnable方法,然后将锁的条件队列中的所有线程放入锁等待队列中,这些线程会依次的获取锁、释放锁,接着先从await方法返回,再从CyclicBarrier的await方法中返回。

Generation是一代的意思,唯一记录了barrier是否broken。看CyclicBarrier的名字也知道,它是可重复使用的,每次使用CyclicBarrier,本次所有线程同属于一代,即同一个Generation。当parties个线程到达barrier时,需要调用nextGeneration更新换代。

barrier被broken后,调用breakBarrier方法,将generation.broken设置为true,并使用signalAll通知所有等待的线程。

JUC回顾之-CyclicBarrier底层实现和原理
https://www.cnblogs.com/200911/p/6060195.html

Java并发包中CyclicBarrier的工作原理、使用示例
https://www.cnblogs.com/nullzx/p/5271964.html

分析同步工具Semaphore和CyclicBarrier的实现原理
https://www.jianshu.com/p/060761df128b

Semaphore

Semaphore翻译成字面意思为 信号量,Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

public Semaphore(int permits) //参数permits表示许可数目,即同时可以允许多少线程进行访问

下面对上面说的三个辅助类进行一个总结:
1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

2)Semaphore其实和锁有点类似,它一般用于控制对某组资源的访问权限。

Java并发编程:CountDownLatch、CyclicBarrier和Semaphore - 海子
http://www.cnblogs.com/dolphin0520/p/3920397.html


阻塞队列

阻塞队列和普通队列的区别?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。

阻塞队列常用操作

阻塞队列对插入和删除操作都提供了4种处理方式:

方法/处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用

抛出异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

聊聊并发(七)——Java中的阻塞队列
http://ifeve.com/java-blocking-queue/


concurrent包中的7种阻塞队列

在java.util.concurrent包下提供了若干个阻塞队列

JDK7提供了7个阻塞队列。分别是
ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列。
LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列。
PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列。
DelayQueue:一个使用优先级队列实现的无界阻塞队列。
SynchronousQueue:一个不存储元素的阻塞队列。
LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。jdk7新增
LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。

聊聊并发(七)——Java中的阻塞队列
http://ifeve.com/java-blocking-queue/

Java并发编程:阻塞队列 - 海子
http://www.cnblogs.com/dolphin0520/p/3932906.html

ArrayBlockingQueue

ArrayBlockingQueue:基于数组实现的一个阻塞队列,在创建ArrayBlockingQueue对象时必须制定容量大小。并且可以指定公平性与非公平性,默认情况下为非公平的,即不保证等待时间最长的队列最优先能够访问队列。
ArrayBlockingQueue(1000,true);//创建公平队列

LinkedBlockingQueue

LinkedBlockingQueue:基于链表实现的一个阻塞队列,在创建LinkedBlockingQueue对象时如果不指定容量大小,则默认大小为Integer.MAX_VALUE。

PriorityBlockingQueue

PriorityBlockingQueue:以上2种队列都是先进先出队列,而PriorityBlockingQueue却不是,它会按照元素的优先级对元素进行排序,按照优先级顺序出队,每次出队的元素都是优先级最高的元素。注意,此阻塞队列为无界阻塞队列,即容量没有上限(通过源码就可以知道,它没有容器满的信号标志),前面2种都是有界队列。

SynchronousQueue

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

DelayQueue

DelayQueue:基于PriorityQueue,一种延时阻塞队列,DelayQueue中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue也是一个无界队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。

LinkedTransferQueue(jdk1.7新增)

LinkedTransferQueue是一个由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。

transfer方法。如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。

tryTransfer方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。

LinkedBlockingDeque

LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。

相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法,以First单词结尾的方法,表示插入,获取(peek)或移除双端队列的第一个元素。以Last单词结尾的方法,表示插入,获取或移除双端队列的最后一个元素。


阻塞队列实现原理(Lock+Condition)

ArrayBlockingQueue源码:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
    final Object[] items; //存储元素的数组
    int takeIndex; //队首元素指针,即下一次take,poll,peek,remove操作的元素索引
    int putIndex; //队尾元素指针,即下一次put,offer,add操作的元素索引
    int count; //队列中元素个数

    final ReentrantLock lock;
    private final Condition notEmpty; //不空条件,当队列为空时调用此条件的await方法等待此条件
    private final Condition notFull; //不满条件,当队列满时调用此条件的await方法等待此条件
}

put()方法:

public void put(E e) throws InterruptedException {
    if (e == null) throw new NullPointerException();
    final E[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        try {
            while (count == items.length)
                notFull.await();
        } catch (InterruptedException ie) {
            notFull.signal(); // propagate to non-interrupted thread
            throw ie;
        }
        insert(e);
    } finally {
        lock.unlock();
    }
}

private void insert(E x) {
    items[putIndex] = x;
    putIndex = inc(putIndex);
    ++count;
    notEmpty.signal();
}

从put方法的实现可以看出,它先获取了锁,并且获取的是可中断锁,然后判断当前元素个数是否等于数组的长度,如果相等,则调用notFull.await()进行等待,如果捕获到中断异常,则唤醒线程并抛出异常。
当数组不满或者当被其他线程唤醒时,通过insert(e)方法插入元素,插入成功后,通过notEmpty唤醒正在等待取元素的线程。

take()方法

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        try {
            while (count == 0)
                notEmpty.await();
        } catch (InterruptedException ie) {
            notEmpty.signal(); // propagate to non-interrupted thread
            throw ie;
        }
        E x = extract();
        return x;
    } finally {
        lock.unlock();
    }
}

private E extract() {
    final E[] items = this.items;
    E x = items[takeIndex];
    items[takeIndex] = null;
    takeIndex = inc(takeIndex);
    --count;
    notFull.signal();
    return x;
}

跟put方法实现很类似,只不过put方法等待的是notFull信号,而take方法等待的是notEmpty信号。

阻塞队列应用:生产者/消费者模式

使用阻塞队列实现的生产者-消费者模式,代码要简单得多,不需要再单独考虑同步和线程间通信的问题,这些阻塞队列都帮我们做了。
只要符合生产者-消费者模型的都可以使用阻塞队列。

public class Test {
    private int queueSize = 10;
    private ArrayBlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(queueSize);

    public static void main(String[] args)  {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();

        producer.start();
        consumer.start();
    }

    class Consumer extends Thread{
        @Override
        public void run() {
            consume();
        }
        private void consume() {
            while(true){
                try {
                    queue.take();
                    System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread{
        @Override
        public void run() {
            produce();
        }
        private void produce() {
            while(true){
                try {
                    queue.put(1);
                    System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

Java并发编程:阻塞队列 - 海子
http://www.cnblogs.com/dolphin0520/p/3932906.html


ScheduledThreadPoolExecutor(定时任务)

ThreadPoolExecutor,它可另行安排在给定的延迟后运行命令,或者定期执行命令。

调度线程池主要用于定时器或者延迟一定时间再执行任务时候使用。内部使用优化的DelayQueue来实现,由于使用队列来实现定时器,有出入队调整堆等操作,所以定时并不是非常非常精确。

我们通过查看ScheduledThreadPoolExecutor的源代码,可以发现ScheduledThreadPoolExecutor的构造器都是调用父类的构造器,只是它使用的工作队列是java.util.concurrent.ScheduledThreadPoolExecutor.DelayedWorkQueue通过名字我们都可以猜到这个是一个延时工作队列.

因为ScheduledThreadPoolExecutor的最大线程是Integer.MAX_VALUE,而且根据源码可以看到execute和submit其实都是调用schedule这个方法,而且延时时间都是指定为0,所以调用execute和submit的任务都直接被执行.

(二十)java多线程之ScheduledThreadPoolExecutor
http://blog.csdn.net/tianshi_kco/article/details/53026196

Timer和TimerTask执行定时任务

Timer是java.util包下的一个类,在JDK1.3的时候被引入,Timer只是充当了一个调度者的角色,真正的任务逻辑是通过一个叫做TimerTask的抽象类完成的,TimerTask也是java.util包下面的类,它是一个实现了Runnable接口的抽象类,包含一个抽象方法run( )方法,需要我们自己去提供具体的业务实现。
Timer类对象是通过其schedule方法执行TimerTask对象中定义的业务逻辑,并且schedule方法拥有多个重载方法提供不同的延迟与周期性服务。

schedule(TimerTask task,long delay,long period)
安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。以近似固定的时间间隔(由指定的周期分隔)进行后续执行。

ScheduledThreadPoolExecutor和Timer对比

单线程
Timer类是通过单线程来执行所有的TimerTask任务的,如果一个任务的执行过程非常耗时,将会导致其他任务的时效性出现问题。而ScheduledThreadPoolExecutor是基于线程池的多线程执行任务,不会存在这样的问题。

Timer线程不捕获异常
Timer类中是不捕获异常的,假如一个TimerTask中抛出未检查异常,Timer类将不会处理这个异常而产生无法预料的错误。这样一个任务抛出异常将会导致整个Timer中的任务都被取消,此时已安排但未执行的TimerTask也永远不会执行了,新的任务也不能被调度(所谓的“线程泄漏”现象)。

基于绝对时间
Timer类的调度是基于绝对的时间的,而不是相对的时间,因此Timer类对系统时钟的变化是敏感的,举个例子,加入你希望任务1每个10秒执行一次,某个时刻,你将系统时间提前了6秒,那么任务1就会在4秒后执行,而不是10秒后。在ScheduledThreadPoolExecutor,任务的调度是基于相对时间的,原因是它在任务的内部存储了该任务距离下次调度还需要的时间

基于以上3个弊端,在JDK1.5或以上版本中,我们几乎没有理由继续使用Timer类,ScheduledThreadPoolExecutor可以很好的去替代Timer类来完成延迟周期性任务。

Timer与ScheduledThreadPoolExecutor
http://blog.csdn.net/diaorenxiang/article/details/38827409

深入理解Java线程池:ScheduledThreadPoolExecutor
https://www.jianshu.com/p/925dba9f5969


Lock

从Java 5之后,在java.util.concurrent.locks包下提供了比synchronized关键字更优秀的线程间同步方式:Lock

Lock接口提供的几种加锁方式

Java并发编程:Lock - 海子
https://www.cnblogs.com/dolphin0520/p/3923167.html

在Lock接口中声明了四个方法来获取锁:lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()。还有个unLock()方法是用来释放锁的。

lock()阻塞

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。
通常使用Lock来进行同步的话,是以下面这种形式去使用的:

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();  //释放锁
}

tryLock()立即(或等待超时后)返回

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

所以,一般情况下通过tryLock来获取锁时要判断tryLock的返回值:

Lock lock = ...;
if(lock.tryLock()) {
    try{
        //处理任务
    }catch(Exception ex){

    }finally{
        lock.unlock();  //释放锁
    }
}else {
    //如果不能获取锁,则直接做其他事情
}

lockInterruptibly()等待时可被中断

  • lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为本身在前面的文章中讲过单独调用interrupt()方法不能中断正在运行过程中的线程,只能中断阻塞过程中的线程。

ReentrantLock

ReentrantLock(重入锁)是jdk的concurrent包提供的一种独占锁的实现。它继承自Dong Lea的 AbstractQueuedSynchronizer(同步器),确切的说是ReentrantLock的一个内部类继承了AbstractQueuedSynchronizer,ReentrantLock只不过是代理了该类的一些方法,可能有人会问为什么要使用内部类在包装一层? 我想是安全的关系,因为AbstractQueuedSynchronizer中有很多方法,还实现了共享锁,Condition(稍候再细说)等功能,如果直接使ReentrantLock继承它,则很容易出现AbstractQueuedSynchronizer中的API被无用的情况。

轻松学习java可重入锁(ReentrantLock)的实现原理
https://blog.csdn.net/yanyan19880509/article/details/52345422


Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition1的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()
调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

Conditon中的await()对应Object的wait();阻塞当前线程并释放锁。
Condition中的signal()对应Object的notify();唤醒一个等待线程。
Condition中的signalAll()对应Object的notifyAll()。唤醒所有等待线程。

Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
http://www.cnblogs.com/dolphin0520/p/3920385.html

怎么理解Condition
http://www.importnew.com/9281.html

Condition实现原理(AQS同步队列)

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列,该队列是Condition对象实现等待/通知功能的关键。

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态

等待
调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中

通知
调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中

Java多线程Condition接口原理详解
https://blog.csdn.net/fuyuwei2015/article/details/72602182

Condition和wait/notify区别

Condition它更强大的地方在于:能够更加精细的控制多线程的休眠与唤醒。
对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition。

Object中的wait(),notify(),notifyAll()方法是和”同步锁”(synchronized关键字)捆绑使用的;而Condition是需要与”互斥锁”/“共享锁”捆绑使用的。

java的Condition 加强版的wait notify
http://huangyunbin.iteye.com/blog/2181493

用Condition实现生产者消费者模式

官方jdk api文档Condition接口中给出的使用示例。

作为一个示例,假定有一个绑定的缓冲区,它支持 put 和 take 方法。如果试图在空的缓冲区上执行 take 操作,则在某一个项变得可用之前,线程将一直阻塞;如果试图在满的缓冲区上执行 put 操作,则在有空间变得可用之前,线程将一直阻塞。我们喜欢在单独的等待 set 中保存 put 线程和 take 线程,这样就可以在缓冲区中的项或空间变得可用时利用最佳规划,一次只通知一个线程。可以使用两个 Condition 实例来做到这一点。

class BoundedBuffer {
  final Lock lock = new ReentrantLock();
  final Condition notFull  = lock.newCondition();
  final Condition notEmpty = lock.newCondition();

  final Object[] items = new Object[100];
  int putptr, takeptr, count;

  public void put(Object x) throws InterruptedException {
    lock.lock();
    try {
      while (count == items.length)
        notFull.await();
      items[putptr] = x;
      if (++putptr == items.length) putptr = 0;
      ++count;
      notEmpty.signal();
    } finally {
      lock.unlock();
    }
  }

  public Object take() throws InterruptedException {
    lock.lock();
    try {
      while (count == 0)
        notEmpty.await();
      Object x = items[takeptr];
      if (++takeptr == items.length) takeptr = 0;
      --count;
      notFull.signal();
      return x;
    } finally {
      lock.unlock();
    }
  }
}

Lock锁和synchronized关键字异同点

1)synchronized是Java语言的关键字,因此是内置特性。Lock是一个类,通过这个类可以实现同步访问;
2)Lock和synchronized有一点非常大的不同,采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放对锁的占用;而Lock则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

在Synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从Synchronized引入了偏向锁,轻量级锁(自旋锁)后,两者的性能就差不多了,在两种方法都可用的情况下,官方甚至建议使用synchronized,其实synchronized的优化我感觉就借鉴了ReenTrantLock中的CAS技术。都是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞。

ReenTrantLock可重入锁(和synchronized的区别)总结
https://www.cnblogs.com/baizhanshi/p/7211802.html

Java并发编程:Lock - 海子
https://www.cnblogs.com/dolphin0520/p/3923167.html


ReadWriteLock

ReadWriteLock 维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独占的。

ReadWriteLock也是一个接口,在它里面只定义了两个方法:
Lock readLock(); //获取读锁
Lock writeLock(); //获取写锁

不过要注意的是,如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
即只有无写锁时读锁间可以同时申请。

如果是排他锁(可重入锁)的话,一个线程加锁后(不论读还是写)另一个线程都不能再加锁(不论读还是写)
如果是读写锁(共享锁)的话,一个线程加读锁后其他线程可加读锁但不能加写锁,一个线程加写锁的话其他线程不能再加任何锁。即读读可并发,读写不可并发。

Java并发编程:Lock - 海子
https://www.cnblogs.com/dolphin0520/p/3923167.html

ReentrantReadWriteLock

ReadWriteLock接口的唯一实现类


锁的相关概念

可重入锁

可重入锁指的是在一个线程中可以多次获取同一把锁,比如:
一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁;

举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。

synchronized和ReentrantLock都是可重入锁。

可中断锁

可中断锁:顾名思义,就是可以响应中断的锁。

在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

lockInterruptibly()的用法时已经体现了Lock的可中断性。

公平锁

公平锁即尽量以请求锁的顺序来获取锁。比如同是有多个线程在等待一个锁,当这个锁被释放时,等待时间最久的线程(最先请求的线程)会获得该所,这种就是公平锁。

非公平锁即无法保证锁的获取是按照请求锁的顺序进行的。这样就可能导致某个或者一些线程永远获取不到锁。

在Java中,synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。

而对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以设置为公平锁。

ReentrantLock的构造方法有个参数ReentrantLock(boolean fair),如果参数为true表示为公平锁,为fasle为非公平锁。默认情况下,如果使用无参构造器,则是非公平锁。

读写锁

读写锁将对一个资源(比如文件)的访问分成了2个锁,一个读锁和一个写锁。

正因为有了读写锁,才使得多个线程之间的读操作不会发生冲突。

ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口。

可以通过readLock()获取读锁,通过writeLock()获取写锁。

Java并发编程:Lock - 海子
https://www.cnblogs.com/dolphin0520/p/3923167.html

可重入锁和不可重入锁(自己设计可重入锁)
https://www.cnblogs.com/dj3839/p/6580765.html


CAS/Atomic/Unsafe

CAS(Compare And Swap)

CAS,Compare And Swap,比较并设置,或比较并交换。

CAS机制简介

CAS 操作中包含三个操作数:需要读写的内存地址V、旧的预期值A、拟写入的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
如果内存位置V的值与预期原值A相等,那么处理器会自动将该位置值更新为新值B。否则处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值)。
CAS机制的意思是:我认为位置 V 应该是值A;如果确实是该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。

CAS用于在硬件层面上提供原子性操作。当前的处理器基本都支持CAS,只不过不同的厂家的实现不一样罢了。在 Intel 处理器中,比较并交换通过指令cmpxchg实现。比较是否和给定的数值一致,如果一致则修改,不一致则不修改。

CAS是一种乐观锁。

Java中的CAS(Java中对乐观锁的实现)

java.util.concurrent.atomic包下的原子操作类都是基于CAS实现的,而CAS就是一种乐观锁。

CAS缺点

CAS的缺点:
1、CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2、不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
3.ABA问题
这是CAS机制最大的问题所在。

ABA问题

尽管CAS看起来很美,但显然这种操作无法涵盖互斥同步的所有使用场景,并且CAS从语义上来说并不是完美的,存在这样的一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。

ABA问题举例:
假设有一个遵循CAS原理的提款机,我现在有10元,打算取出来5元,取钱线程由于某种原因阻塞了,这时我妈给我打了5元,余额为15元,又有一个线程从我账户扣款5元,余额为10元。然后取钱线程继续执行,compare and set的时候一看原值是10元,就认为没问题,更新账户余额为5元。其实整个过程中我损失5元。

漫画:什么是CAS机制?(进阶篇)
https://blog.csdn.net/bjweimengshu/article/details/79000506

漫画:什么是 CAS 机制? - 程序员小灰
https://blog.csdn.net/bjweimengshu/article/details/78949435

CAS与synchronized对比

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS与Synchronized的使用情景:   
1、对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
2、对于资源竞争严重(线程冲突严重)的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

补充: synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

Java并发问题–乐观锁与悲观锁以及乐观锁的一种实现方式-CAS
https://www.cnblogs.com/qjjazry/p/6581568.html


Atomic原子操作类

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

原子操作实现原理(Unsafe类提供的CAS)

以AtomicInteger为例,jdk8中源码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();//获取Unsafe类
    private static final long valueOffset; //value值的内存偏移(内存地址)

    //静态初始化,获取value的内存偏移
    static {
        try {
            valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    private volatile int value; //存储int值的变量,volatile保证当前值是内存中的最新值(可见性)

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    //以原子方式将当前值加,返回以前的值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //以原子方式将当前值加,返回更新的值
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    ...
}

原子类内部使用了Unsafe类进行原子操作
什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFieldOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单地把valueOffset理解为value变量的内存地址。

Unsafe类的getAndAddInt()方法源码(jdk1.8):
var1:当前AtomicInteger对象
var2:内存地址或内存偏移量valueOffset
var4:要增加的值(自增1的话就是1)

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);//根据当前AtomicInteger对象和内存偏移量获取旧值,放到var5中
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//CAS自旋(无限循环)不断尝试更新

    return var5;
}

//native方法compareAndSwapInt()
//var1:当前对象,var2:内存地址,var4:预期的旧值,var5:拟更新的新值
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到Unsafe类的getAndAddInt()方法是一个无限循环,也就是CAS的自旋,循环体当中做了以下几件事:
1、根据当前AtomicInteger对象和内存偏移量valueOffset获取旧值
2、调用compareAndSwapInt()不断尝试更新为新值,如果成功则跳出循环,若失败则不断重复

其中Unsafe类的compareAndSwapInt()方法就是一个CAS原子操作, 内部利用JNI(Java Native Interface)来完成CPU指令的操作:

AtomicStampedReference(解决ABA问题,加版本号)

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

public boolean compareAndSet(V expectedReference,
                            V newReference,
                            int expectedStamp,
                            int newStamp)

如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值。

聊聊并发(五)原子操作的实现原理 - 方 腾飞
http://ifeve.com/atomic-operation/


synchronized关键字

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
  3. 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  4. 修饰一个类,其作用范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象。

1)当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

2)当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

3)如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

synchronized锁是加在对象上的

synchronized非静态方法:当前调用此方法的实例对象

所有的非静态同步方法用的都是同一把锁——当前调用此方法的实例对象本身,也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。

synchronized void method{} 功能上,等效于
void method{
synchronized(this) {
    …
}
}

Java中每个对象都有一个内置锁
当程序运行到非静态的synchronized同步方法上时,自动获得与正在执行代码类的当前实例(this实例)有关的锁。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。
当程序运行到synchronized同步方法或代码块时才该对象锁才起作用。
一个对象只有一个锁。所以,如果一个线程获得该锁,就没有其他线程可以获得锁,直到第一个线程释放(或返回)锁。这也意味着任何其他线程都不能进入该对象上的synchronized方法或代码块,直到该锁被释放。

synchronized静态方法的锁:当前类对象

而所有的静态同步方法用的也是同一把锁——方法所在的类对象本身(Class对象),这两把锁(之实例对象和类对象)是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
即:
synchronized static void method() 等效于
static void method{
synchronized(Obj.class)
}
}

静态同步方法和非静态同步方法将永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

synchronized代码块的锁:括号中的对象

而对于同步块,由于其锁是可以选择的,所以只有使用同一把锁的同步块之间才有着竞态条件,这就得具体情况具体分析了。

对于同步代码块,要看清楚什么对象已经用于锁定(synchronized后面括号的内容)。在同一个对象上进行同步的线程将彼此阻塞,在不同对象上锁定的线程将永远不会彼此阻塞。

所以,如果synchronized代码块和synchronized方法的锁是同一个,他们之间也会互斥:
synchronized(非this对象x)格式的写法是将x对象本身作为对象监视器,有三个结论得出:
1、当多个线程同时执行synchronized(x){}同步代码块时呈同步效果
2、当其他线程执行x对象中的synchronized同步方法时呈同步效果
3、当其他线程执行x对象方法中的synchronized(this)代码块时也呈同步效果
因为他们申请的锁都是对象x这个实例

java 多线程9 : synchronized锁机制 之 代码块锁 synchronized同步代码块
https://www.cnblogs.com/signheart/p/0a8548258725cb8812768d2b3e1a2aef.html

synchronized常见面试题(分析申请的锁是什么)

问:一个类中两个方法上分别有synchronized,两个线程分别调用这两个方法,会阻塞吗?
答:普通方法的锁是实例对象,那这个问题得先确认下是不是通过同一个对象调用的,如果通过同一个对象调用,会阻塞;如果通过不同对象调用,不会阻塞。
比如一个自定义Runnable对象,里面有两个synchronized方法,把同一个Runnable对象分别传给两个Thread,两个线程中分别调用这两个方法,则他们之间会有竞争关系,因为申请的锁是同一个。但如果把两个不同的Runnable实例分别传给两个thread,则不会有竞争。

问:类中有个synchronized静态方法和一个synchronized非静态方法,两个线程同时访问,会存在竞争问题吗?
答:不会,占用的锁不同(这种题就首先分析他需要获取什么锁)
如果一个线程执行一个对象的非static synchronized方法,另外一个线程需要执行这个对象所属类的static synchronized方法,此时不会发生互斥现象,因为访问static synchronized方法占用的是类锁,而访问非static synchronized方法占用的是对象锁,所以不存在互斥现象。

问:类中有个synchronized静态方法,在两个线程中分别通过两个不同的对象调用此静态方法,会阻塞吗?
答:会,因为占用的锁是同一个锁,都是类对象本身。

同步块内部修改了同步对象
Synchronized块同步变量的误区
https://blog.csdn.net/magister_feng/article/details/6627523

同步块局部变量

使用synchronized需要注意的一个问题
https://blog.csdn.net/jimmylincole/article/details/17194337

Java并发编程:synchronized - 海子
http://www.cnblogs.com/dolphin0520/p/3923737.html

java synchronized静态同步方法与非静态同步方法,同步语句块
https://www.cnblogs.com/csniper/p/5478572.html


synchronized与Lock的区别

1、synchronized是关键字,Lock是一组类和接口
2、synchronized无法判断锁状态,会阻塞线程。Lock可判断锁状态,提供tryLock方法尝试获取锁
3、synchronized不需要手动释放锁。Lock需要释放锁。
4、synchronized在线程异常时会被系统自动释放掉,不会死锁。Lock必须在finally中手动释放锁,否则可能死锁。
5、synchronized和Lock都是可重入锁。
6、synchronized是非公平锁。Lock默认非公平,可设置为公平锁。
7、synchronized无法被中断。Lock提供可被中断的加锁方法。

详解synchronized与Lock的区别与使用
http://blog.csdn.net/u012403290/article/details/64910926?locationNum=11&fps=1


synchronized实现原理(监视器monitor)

在Java中,最基本的互斥同步手段就是synchronized关键字,synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那就是这个对象的reference;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,去取对应的对象实例或Class对象来作为锁对象。

Java高效并发之乐观锁悲观锁、(互斥同步、非互斥同步)
http://blog.csdn.net/truelove12358/article/details/54963791

从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。对于synchronized方法,执行中的线程识别该方法的 method_info 结构是否有 ACC_SYNCHRONIZED 标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

Java对象头

synchronized使用的锁对象是存储在Java对象头里的,或者说,synchronized锁就是对象头里的标志位

重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

锁存在Java对象头里。如果对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。

聊聊并发(二)Java SE1.6中的Synchronized
http://ifeve.com/java-synchronized/

监视器monitor

深入理解Java并发之synchronized实现原理
http://blog.csdn.net/javazejian/article/details/72828483

死磕Java并发:深入分析synchronized的实现原理
http://www.importnew.com/23511.html


java6对synchronized的优化

JDK1.6提供了大量的锁优化技术,其中包括适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。通过使用这些方法对Synchronized进行了虚拟机级别的锁优化,从而提高了Synchronized的使用效率。

自旋锁和自适应自旋(盲等代替阻塞)

在进行互斥和同步时,互斥同步对性能最大的影响是阻塞的的实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统的并发性能带来了很大的压力。自旋锁意味着,当存在两个或者两个以上的线程同时并行执行,让后面请求锁的线程“稍等下”,但是不放弃处理器执行时间,看看持有锁的线程是否很快的释放锁,此时我们让等待锁的线程执行一个忙循环(自旋)进行等待。这种让线程使用忙循环或者自旋的形式等待线程锁的技术就成为自旋锁。

自旋锁的理论基础在于,共享数据的锁定状态只会持续很短一段时间,故不需要为了这段时间去挂起和恢复线程。不过自旋等待虽然避免了线程切换的开销,但是它还是会占用处理器的时间,所以自旋的时间或者次数是有限的。

自适应自旋的自旋时间是不固定的,而是通过前一次在同一个锁上的自旋时间及锁得持有者的状态决定的。如果在同一个锁对象上,自旋等待刚刚成功获取过锁,且持有锁的线程正在运行,那么认为这次也成功获取锁的概率非常大,那么就允许自旋等待更长的时间来获取这个锁。如果对于某个锁,自旋很少成功,那么以后对于这个锁就省略掉自旋过程。

锁消除(单线程锁优化)

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享竞争的锁进行消除。例如我们在单线程中使用Hashtable或者StringBuffer进行编程(它们中的大部分方法都是synchronized的),因为不存在锁竞争,所以这些同步方法上的同步就可以被自动消除。

锁粗化(防止频繁加解锁)

原则上我们写代码会尽量将同步块的作用范围限制在最小(只对共享数据进行同步)。但是如果一系列连续操作都对同一个对象反复加锁和解锁(类似于在循环体中使用synchronized),即使没有竞争,也会导致性能损失。那么将锁的范围扩展(粗化)到整个操作之外(类似于将synchronized放在循环体之外),这样就只需要一次加锁了。

锁升级(偏向->轻量->重量)

Java6中的锁一共有四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它们会随着竞争情况逐渐升级,但是不能够降级。
锁升级的过程大概是这样的,刚开始处于无锁状态,当线程第一次申请时,会先进入偏向锁状态,然后如果出现锁竞争,就会升级为轻量级锁(这升级过程中可能会牵扯自旋锁),如果轻量级锁还是解决不了问题,则会进入重量级锁状态,从而彻底解决并发的问题。

轻量级锁(无竞争时用CAS代替加锁)

轻量级锁加锁过程:当代码进入同步块时,如果同步对象没有被锁定(锁标志位为“01”),JVM会首先在当前线程的栈帧中建立一个用于存储锁记录(Lock Record)的空间,并将对象头中的Mark Word复制到锁记录中,官方称这份拷贝为Displaced Mark Word。然后虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针。如果更新成功,那么这个线程就拥有了该对象的锁,并且对象的Mark Word的“锁标志位”将转变为“00”,即表示此对象处于轻量级锁定状态;如果失败,JVM会首先检查对象Mark Word是否指向当前对象的栈帧,如果当前线程已经拥有了这个对象的锁,就可以直接进入同步块,否则就表示其他线程竞争锁,当前线程便尝试使用自旋来等待锁。当自旋等待一定次数时,此时属于多个线程竞争同一个锁,轻量级锁就不再有效,就要膨胀为重量级锁。
当只有一个线程即不存在竞争时,轻量级锁还是要加锁解锁的,只不过用CAS操作代替了锁,进入synchronized代码块的时候加锁,退出synchronized代码块的时候解锁

偏向锁(无竞争时加锁后就不解锁)

偏向锁的目的是为了消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程永远不需要再进行同步。
更具体的过程是:当锁对象第一次被线程获取的时候,虚拟将将会把对象头中的标志位设置为“01”,即偏向模式,然后使用CAS操作将这个锁的线程ID记录在对象的Mark Word中,如果操作成功,则持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作。当另外一个线程尝试获取这个锁时,偏向锁模式宣告结束。根据对象当前是否处于锁定状态,将会进一步撤销偏向恢复未锁定状态(标志为“01”)或者进入轻量级锁定(状态为“00”)状态。
当只有一个线程即不存在竞争时,偏向锁用CAS操作把锁分配给线程后,就不再解锁,即使退出synchronized代码块也不解锁,直到有其他线程要获取此对象锁时才解锁并退出偏向锁模式

Java6中与Synchronized相关的锁机制
http://blog.csdn.net/teaandnoodle/article/details/52229258

聊聊并发(二)Java SE1.6中的Synchronized
http://ifeve.com/java-synchronized/

synchronized实现原理
https://www.cnblogs.com/pureEve/p/6421273.html


volatile关键字

关键字volatile可以说是Java虚拟机提供的最轻量级的同步机制。一个变量被定义为volatile后,它将具备两种特性:
1、保证此变量对所有线程的”可见性”,所谓”可见性”是指当一条线程修改了这个变量的值,新值对于其它线程来说都是可以立即得知的,而普通变量不能做到这一点,普通变量的值在在线程间传递均需要通过主内存来完成。
2、使用volatile变量的第二个语义是禁止指令重排序优化(访问volatile变量的语句不会被指令重排),普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。这点参见单例模式中的双重检测实现方式

//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;      //语句5

由于flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句1、语句2前面,也不会讲语句3放到语句4、语句5后面。但是要注意语句1和语句2的顺序、语句4和语句5的顺序是不作任何保证的。

3、volatile修饰的变量符合happens before原则,即对这个变量的写操作发生于读操作之前

总结一下Java内存模型对volatile变量定义的特殊规则:
1、在工作内存中,每次使用某个变量的时候都必须线从主内存刷新最新的值,用于保证能看见其他线程对该变量所做的修改之后的值。
2、在工作内存中,每次修改完某个变量后都必须立刻同步回主内存中,用于保证其他线程能够看见自己对该变量所做的修改。
3、volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序顺序相同。

volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存

Java并发编程:volatile关键字解析 - 海子
http://www.cnblogs.com/dolphin0520/p/3920373.html

volatile实现原理(内存屏障,缓存)

访问volatile变量的汇编指令前会多出一个lock前缀指令,

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。

Java并发编程:volatile关键字解析 - 海子
http://www.cnblogs.com/dolphin0520/p/3920373.html

聊聊并发(一)深入分析Volatile的实现原理
http://ifeve.com/volatile/

volatile保证可见性但不保证原子性

用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。

意思就是说,如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次jvm都会读取最新写入的值并使其最新值在所有CPU可见。volatile似乎是有时候可以代替简单的锁,似乎加了volatile关键字就省掉了锁。但又说volatile不能保证原子性(java程序员很熟悉这句话:volatile仅仅用来保证该变量对所有线程的可见性,但不保证原子性)。这不是互相矛盾吗?

1.Volatile不具有原子性
2.告诉jvm该变量为所有线程共享的,Cpu执行时不进行线程间上下文环境切换,提高效率
3.不要将volatile用在getAndOperate场合,仅仅set或者get的场景是适合volatile的

经典的多线程i++问题

例如:volatile int i=0; 有10个线程同时做i++操作,最终结果是几?
答:小于等于10
原因:自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。volatile只能保证可见性,不能保证对其操作的原子性。
比如有两个线程A和B对volatile修饰的i进行i++操作,i的初始值是0,A线程执行i++时刚读取了i的值0,就切换到B线程了,B线程(从内存中)读取i的值也为0,然后就切换到A线程继续执行i++操作,完成后i就为1了,接着切换到B线程,因为之前已经读取过了,所以继续执行i++操作,最后的结果i就为1了。同理可以解释为什么每次运行结果都是小于10的数字。

再来一遍解释:
线程1先读取了变量i的原始值,然后线程1被阻塞了;线程2也去读取变量i的原始值,然后进行加1操作,并把+1后的值写入工作内存,最后写入主存,然后线程1接着进行加1操作,由于已经读取了i的值,此时在线程1的工作内存中i的值仍然是之前的值,所以线程1对i进行加1操作后的值和刚才一样,然后将这个值写入工作内存,最后写入主存。这样就出现了两个线程自增完后其实只加了一次。究其原因是因为volatile不能保证原子性。

jvm中有一个内存区域是jvm虚拟机栈,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。


线程工作内存和主内存

read and load 从主存复制变量到当前工作内存
use and assign 执行代码,改变共享变量值
store and write 用工作内存数据刷新主存相关内容
其中use and assign 可以多次出现

但是这一些操作并不是原子性,也就是 在read load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,所以计算出来的结果会和预期不一样

对于volatile修饰的变量,jvm虚拟机只是保证从主内存加载到线程工作内存的值是最新的
例如假如线程1,线程2 在进行read,load 操作中,发现主内存中count的值都是5,那么都会加载这个最新的值
在线程1堆count进行修改之后,会write到主内存中,主内存中的count变量就会变为6
线程2由于已经进行read,load操作,在进行运算之后,也会更新主内存count的变量值为6
导致两个线程及时用volatile关键字修改之后,还是会存在并发的情况。

如何解决?

加synchronized锁(阻塞,性能低)

使用synchronized对i++操作加锁,就能保证同一时刻只有一个线程获取锁然后执行同步代码。运行结果必然是10。
加了同步锁之后,count自增的操作变成了原子性操作,所以最终的输出一定是count=10,代码实现了线程安全。
但使用synchronized锁有性能问题
Synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。
尽管Java1.6为Synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然较低。

使用AtomicInteger原子操作类(CAS机制,乐观锁)

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。例如AtomicBoolean,AtomicInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

public static AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet();//原子自增

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

漫画:什么是 CAS 机制? - 程序员小灰
https://blog.csdn.net/bjweimengshu/article/details/78949435

【多线程系列】Volatile总结之同步问题
http://blog.csdn.net/gooooooal/article/details/50014341

Java并发——线程同步Volatile与Synchronized详解
http://blog.csdn.net/seu_calvin/article/details/52370068

JAVA并发编程4_线程同步之volatile关键字
https://www.cnblogs.com/qhyuan1992/p/5385309.html


volatile应用场景

做while循环的状态标记量

比如做while循环是否继续的标记量

private volatile boolean flag= true;
protected void stopTask() {
    flag = false;
}

@Override
public void run() {
    while (flag) {
        // 执行任务…
    }
}

配合双重检查实现单例模式

Double-Check单例模式中,为了避免同步代码块外的if (singleton == null)判断可能看到初始化不完成整的实例(不是null但未初始化完成),必须将singleton变量定义为volatile的,以避免jvm指令重排。

public class Singleton {
    private static volatile Singleton singleton=null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

单例模式与双重检测
http://www.iteye.com/topic/652440

volatile和synchronized的区别

volatile和synchronized的区别
http://blog.csdn.net/suifeng3051/article/details/52611233

锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。

首先需要理解线程安全的两个方面:执行控制和内存可见。

  • 执行控制的目的是控制代码执行(顺序)及是否可以并发执行。
  • 内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个内存屏障,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都happens-before于随后获得这个锁的线程的操作。

volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

正确使用 Volatile 变量
https://www.ibm.com/developerworks/cn/java/j-jtp06197.html


happens-before保证可见性

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

happens-before原则定义如下:

  1. 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  2. 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制定的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

下面是happens-before原则规则:
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作;
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C;
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

【死磕Java并发】—–Java内存模型之happens-before
https://www.cnblogs.com/chenssy/p/6393321.html


Fork/Join框架

Fork/Join框架是Java7提供的一个用于并行执行任务的框架,作者是并发大神Doug Lea,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。

Fork/Join框架要完成两件事情:

  • 第一步分割任务。
    首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停的分割,直到分割出的子任务足够小。
  • 第二步执行任务并合并结果。
    分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

Fork/Join使用两个类来完成以上两件事情:

  • ForkJoinTask:我们要使用ForkJoin框架,必须首先创建一个ForkJoin任务。它提供在任务中执行fork()和join()操作的机制,通常情况下我们不需要直接继承ForkJoinTask类,而只需要继承它的子类,Fork/Join框架提供了以下两个子类:
    • RecursiveAction:用于没有返回结果的任务。
    • RecursiveTask:用于有返回结果的任务。
  • ForkJoinPool :ForkJoinTask需要通过ForkJoinPool来执行,任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务。

Fork/Join的典型用法如下:

Result solve(Problem problem) {
    if (problem is small)
        directly solve problem
    else {
        split problem into independent parts
        fork new subtasks to solve each part
        join all subtasks
        compose result from subresults
    }
}

Fork/Join实现原理

ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责存放程序提交给ForkJoinPool的任务,而ForkJoinWorkerThread数组负责执行这些任务。

ForkJoinTask的fork方法实现原理:
当我们调用ForkJoinTask的fork方法时,程序会调用ForkJoinWorkerThread的pushTask方法异步的执行这个任务,pushTask方法把当前任务存放在ForkJoinTask 数组queue里。然后再调用ForkJoinPool的signalWork()方法唤醒或创建一个工作线程来执行任务。

ForkJoinTask的join方法实现原理:
Join方法的主要作用是阻塞当前线程并等待获取结果。
首先,它调用了doJoin()方法,通过doJoin()方法得到当前任务的状态来判断返回什么结果,任务状态有四种:已完成(NORMAL),被取消(CANCELLED),信号(SIGNAL)和出现异常(EXCEPTIONAL)。
如果任务状态是已完成,则直接返回任务结果。
如果任务状态是被取消,则直接抛出CancellationException。
如果任务状态是抛出异常,则直接抛出对应的异常。
在doJoin()方法里,首先通过查看任务的状态,看任务是否已经执行完了,如果执行完了,则直接返回任务状态,如果没有执行完,则从任务数组里取出任务并执行。如果任务顺利执行完成了,则设置任务状态为NORMAL,如果出现异常,则纪录异常,并将任务状态设置为EXCEPTIONAL。

聊聊并发(八)——Fork/Join框架介绍
http://ifeve.com/talk-concurrency-forkjoin/


工作窃取(work stealing)

工作窃取(work-stealing)算法是指某个线程从其他队列里窃取任务来执行。

那么为什么需要使用工作窃取算法呢?
假如我们需要做一个比较大的任务,我们可以把这个任务分割为若干互不依赖的子任务,为了减少线程间的竞争,于是把这些子任务分别放到不同的队列里,并为每个队列创建一个单独的线程来执行队列里的任务,线程和队列一一对应,比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完,而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着,不如去帮其他线程干活,于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列,所以为了减少窃取任务线程和被窃取任务线程之间的竞争,通常会使用双端队列,被窃取任务线程永远从双端队列的头部拿任务执行,而窃取任务的线程永远从双端队列的尾部拿任务执行。

工作窃取算法的优点是充分利用线程进行并行计算,并减少了线程间的竞争,其缺点是在某些情况下还是存在竞争,比如双端队列里只有一个任务时。并且消耗了更多的系统资源,比如创建多个线程和多个双端队列。

聊聊并发(八)——Fork/Join框架介绍
http://ifeve.com/talk-concurrency-forkjoin/


Fork/Join使用示例(继承RecursiveTask)

我们通过一个简单的例子来介绍一下Fork/Join框架的使用。需求是求1+2+3+4的结果
使用Fork/Join框架首先要考虑到的是如何分割任务,如果希望每个子任务最多执行两个数的相加,那么我们设置分割的阈值是2,由于是4个数字相加,所以Fork/Join框架会把这个任务fork成两个子任务,子任务一负责计算1+2,子任务二负责计算3+4,然后再join两个子任务的结果。因为是有结果的任务,所以必须继承RecursiveTask,实现代码如下:

package concurrent_test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

public class ForkJoinCountTask extends RecursiveTask<Integer> {
    private static final int THREAD_HOLD = 2; //任务分割阈值
    private int start;
    private int end;

    public ForkJoinCountTask(int start,int end){
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        System.out.println(Thread.currentThread());
        int sum = 0;
        //如果任务足够小就计算
        boolean canCompute = (end - start) <= THREAD_HOLD;
        if(canCompute){
            for(int i=start;i<=end;i++){
                sum += i;
            }
        }else{
            //如果任务大于阀值,就分裂成两个子任务计算
            int middle = (start + end) / 2;
            ForkJoinCountTask left = new ForkJoinCountTask(start,middle);
            ForkJoinCountTask right = new ForkJoinCountTask(middle+1,end);
            //执行子任务
            /* 注意!下面这种两个子任务分别fork的执行方法是低效的,相当于当前线程不干活,把任务拆分后都分给新开的两个线程了
            left.fork();
            right.fork();
            正确的写法是:invokeAll(left, right);
            */
            invokeAll(left,right);
            //获取子任务结果
            int lResult = left.join();
            int rResult = right.join();
            //合并子任务结果
            sum = lResult + rResult;
        }
        return sum;
    }

    public static void main(String[] args){
        ForkJoinPool pool = new ForkJoinPool();
        //生成一个计算任务,负责计算1+2+3+4
        ForkJoinCountTask task = new ForkJoinCountTask(1,4);
        //执行一个任务
        Future<Integer> result = pool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

通过这个例子让我们再来进一步了解ForkJoinTask,ForkJoinTask与一般的任务的主要区别在于它需要实现compute方法,在这个方法里,首先需要判断任务是否足够小,如果足够小就直接执行任务。如果不足够小,就必须分割成两个子任务,每个子任务在调用fork方法时,又会进入compute方法,看看当前子任务是否需要继续分割成孙任务,如果不需要继续分割,则执行当前子任务并返回结果。使用join方法会等待子任务执行完并得到其结果。

Fork/Join框架详解
https://www.cnblogs.com/senlinyang/p/7885964.html


使用invokeAll()代替每个子任务fork()

新手在编写Fork/Join任务时,往往用搜索引擎搜到一个例子,然后就照着例子写出了下面的代码:

protected Long compute() {
    if (任务足够小?) {
        return computeDirect();
    }
    // 任务太大,一分为二:
    SumTask subtask1 = new SumTask(...);
    SumTask subtask2 = new SumTask(...);
    // 分别对子任务调用fork():
    subtask1.fork();
    subtask2.fork();
    // 合并结果:
    Long subresult1 = subtask1.join();
    Long subresult2 = subtask2.join();
    return subresult1 + subresult2;
}

这种写法是低效的(也不能说错误,只不过多开了不必要的线程)。
这是因为执行compute()方法的线程本身也是一个Worker线程,当对两个子任务调用fork()时,这个Worker线程就会把任务分配给另外两个Worker,但是它自己却停下来等待不干活了!这样就白白浪费了Fork/Join线程池中的一个Worker线程。

其实,我们查看JDK的invokeAll()方法的源码就可以发现,invokeAll的N个任务中,其中N-1个任务会使用fork()交给其它线程执行,但是,它还会留一个任务自己执行,这样,就充分利用了线程池,保证没有空闲的不干活的线程。

ForkJoinTask中两个参数的invokeAll的源码:

public static void invokeAll(ForkJoinTask<?> t1, ForkJoinTask<?> t2) {
    int s1, s2;
    t2.fork();
    if ((s1 = t1.doInvoke() & DONE_MASK) != NORMAL)
        t1.reportException(s1);
    if ((s2 = t2.doJoin() & DONE_MASK) != NORMAL)
        t2.reportException(s2);
}

Java的Fork/Join任务,你写对了吗? - 廖雪峰
https://www.liaoxuefeng.com/article/001493522711597674607c7f4f346628a76145477e2ff82000


Fork/Join框架的异常处理

ForkJoinTask在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以ForkJoinTask提供了isCompletedAbnormally()方法来检查任务是否已经抛出异常或已经被取消了,并且可以通过ForkJoinTask的getException方法获取异常。使用如下代码:

if(task.isCompletedAbnormally()) {
    System.out.println(task.getException());
}

getException()方法返回Throwable对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回null。

ForkJoinTask中getException()方法源码如下:

public final Throwable getException() {
    int s = status & DONE_MASK;
    return ((s >= NORMAL)    ? null :
            (s == CANCELLED) ? new CancellationException() :
            getThrowableException());
}

Fork/Join框架详解
https://www.cnblogs.com/senlinyang/p/7885964.html


上一篇 Java面试准备-(04)JVM

下一篇 Java面试准备-(02)集合框架

阅读
34,671
阅读预计130分钟
创建日期 2018-03-20
修改日期 2019-01-11
类别
目录
  1. java 多线程与并发(concurrent包)
    1. ConcurrentHashMap
      1. ConcurrentHashMap内部实现(jdk1.7)
      2. put和remove操作(只能链表头部插入)
      3. ConcurrentHashMap的get为什么可以不加锁?
        1. 为什么读取到的结点value有可能是空的?
      4. ConcurrentHashMap不能保证完全线程安全
      5. java8对ConcurrentHashMap的改进
      6. Unsafe与CAS
    2. CopyOnWriteArrayList
      1. CopyOnWriteArrayList与Collections.synchronizedList对比
    3. 创建并使用线程Thread
      1. java线程的5种状态及转换
        1. 新建(NEW)
        2. 可运行(RUNNABLE)或就绪
        3. 运行(RUNNING)
        4. 阻塞(BLOCKED)
          1. 等待阻塞(wait)
          2. 同步阻塞(等待synchronized,Lock)
          3. 其他阻塞(sleep等)
        5. 死亡(DEAD)
      2. Thread类常用方法
        1. sleep睡眠阻塞不释放锁
        2. yield交出时间片不阻塞不释放锁
        3. join阻塞其他线程
          1. join方法会使得被阻塞线程释放对象锁吗
        4. wait阻塞释放锁
        5. notify唤醒1个等待此对象的线程
        6. interrupt中断
          1. 中断处于阻塞状态的线程
          2. 中断处于运行状态的线程
      3. 线程优先级与守护线程
        1. java中线程的优先级
          1. 优先级高的线程不一定先执行
        2. 守护线程与非守护线程的区别
          1. 区别守护进程与守护线程
      4. java中创建线程的三种方法
        1. 继承Thread类,重写run方法
        2. 实现Runnable接口,放入Thread类中
          1. 为什么不能直接调用Runnable接口的run()方法运行?
        3. 使用Callable和Future
        4. 三种创建线程方法对比
    4. 线程工厂
      1. 创建Thread需要哪些参数
      2. 使用线程工厂创建线程有什么好处?
      3. ThreadFactory
      4. Executors的默认线程工厂
    5. Callable
      1. 利用FutureTask封装Callable再由Thread启动
      2. 利用ExecutorService的submit提交Callable任务到线程池执行
      3. Runnable接口和Callable接口的异同点
    6. Future
      1. FutureTask
    7. ThreadLocal
      1. 为什么要使用ThreadLocal变量?
      2. ThreadLocal的四个方法
      3. ThreadLocal实现原理
      4. 什么时候使用ThreadLocal?
    8. ThreadPoolExecutor自定义线程池
      1. 构造方法参数
      2. 有哪几种阻塞队列?
      3. 向线程池提交任务的两种方法(execute,submit)
        1. execute() Runnable任务
        2. submit() Callable任务
      4. 何时创建新线程?(队列与池交互)
      5. 拒绝任务处理策略(饱和策略)
      6. 线程池状态(如何关闭线程池?)
      7. 使用示例
      8. 合理配置线程池的大小
      9. 多线程和线程池(为什么要用线程池?)
    9. Executors自带线程池
    10. 并发编程辅助工具
      1. CountDownLatch
      2. CyclicBarrier
        1. CountDownLatch和CyclicBarrier区别(可重用)
        2. CyclicBarrier原理(Lock,计数器,Generation换代)
      3. Semaphore
    11. 阻塞队列
      1. 阻塞队列和普通队列的区别?
      2. 阻塞队列常用操作
      3. concurrent包中的7种阻塞队列
        1. ArrayBlockingQueue
        2. LinkedBlockingQueue
        3. PriorityBlockingQueue
        4. SynchronousQueue
        5. DelayQueue
        6. LinkedTransferQueue(jdk1.7新增)
        7. LinkedBlockingDeque
      4. 阻塞队列实现原理(Lock+Condition)
      5. 阻塞队列应用:生产者/消费者模式
    12. ScheduledThreadPoolExecutor(定时任务)
      1. Timer和TimerTask执行定时任务
      2. ScheduledThreadPoolExecutor和Timer对比
    13. Lock
      1. Lock接口提供的几种加锁方式
        1. lock()阻塞
        2. tryLock()立即(或等待超时后)返回
        3. lockInterruptibly()等待时可被中断
      2. ReentrantLock
      3. Condition
        1. Condition实现原理(AQS同步队列)
        2. Condition和wait/notify区别
        3. 用Condition实现生产者消费者模式
      4. Lock锁和synchronized关键字异同点
    14. ReadWriteLock
      1. ReentrantReadWriteLock
    15. 锁的相关概念
      1. 可重入锁
      2. 可中断锁
      3. 公平锁
      4. 读写锁
    16. CAS/Atomic/Unsafe
      1. CAS(Compare And Swap)
        1. CAS机制简介
        2. Java中的CAS(Java中对乐观锁的实现)
        3. CAS缺点
          1. ABA问题
        4. CAS与synchronized对比
      2. Atomic原子操作类
        1. 原子操作实现原理(Unsafe类提供的CAS)
        2. AtomicStampedReference(解决ABA问题,加版本号)
    17. synchronized关键字
      1. synchronized锁是加在对象上的
        1. synchronized非静态方法:当前调用此方法的实例对象
        2. synchronized静态方法的锁:当前类对象
        3. synchronized代码块的锁:括号中的对象
        4. synchronized常见面试题(分析申请的锁是什么)
      2. synchronized与Lock的区别
      3. synchronized实现原理(监视器monitor)
        1. Java对象头
        2. 监视器monitor
      4. java6对synchronized的优化
        1. 自旋锁和自适应自旋(盲等代替阻塞)
        2. 锁消除(单线程锁优化)
        3. 锁粗化(防止频繁加解锁)
        4. 锁升级(偏向->轻量->重量)
        5. 轻量级锁(无竞争时用CAS代替加锁)
        6. 偏向锁(无竞争时加锁后就不解锁)
    18. volatile关键字
      1. volatile实现原理(内存屏障,缓存)
      2. volatile保证可见性但不保证原子性
      3. 经典的多线程i++问题
        1. 加synchronized锁(阻塞,性能低)
        2. 使用AtomicInteger原子操作类(CAS机制,乐观锁)
      4. volatile应用场景
        1. 做while循环的状态标记量
        2. 配合双重检查实现单例模式
      5. volatile和synchronized的区别
      6. happens-before保证可见性
    19. Fork/Join框架
      1. Fork/Join实现原理
        1. 工作窃取(work stealing)
      2. Fork/Join使用示例(继承RecursiveTask)
        1. 使用invokeAll()代替每个子任务fork()
      3. Fork/Join框架的异常处理
百度推荐