JAVA并发(下)

J.U.C并发包

Java并发包基石-AQS详解    队列同步器(AbstractQueuedSynchronizer)

https://javadoop.com/post/AbstractQueuedSynchronizer

https://javadoop.com/post/AbstractQueuedSynchronizer-2

https://javadoop.com/post/AbstractQueuedSynchronizer-3

Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrantLock、Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS。AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。

AQS是JUC中很多同步组件的构建基础,简单来讲,它内部实现主要是状态变量state一个FIFO队列

同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点(或共享式或独占式)加入到同步队列尾部(采用自旋CAS来保证此操作的线程安全),随后线程会阻塞;释放时唤醒头结点的后继结点,使其加入对同步状态的争夺中。

HashMap的线程不安全性!

1、put的时候导致的多线程数据不一致。

比如有两个线程A和B,首先A希望插入一个key-value对到HashMap中,首先计算记录所要落到的桶的索引坐标,然后获取到该桶里面的链表头结点,此时线程A的时间片用完了,而此时线程B被调度得以执行,和线程A一样执行,只不过线程B成功将记录插到了桶里面,假设线程A插入的记录计算出来的桶索引和线程B要插入的记录计算出来的桶索引是一样的,那么当线程B成功插入之后,线程A再次被调度运行时,它依然持有过期的链表头但是它对此一无所知,以至于它认为它应该这样做,如此一来就覆盖了线程B插入的记录,这样线程B插入的记录就凭空消失了,造成了数据不一致的行为。

2、另外一个比较明显的线程不安全的问题是HashMap的get操作可能因为resize而引起死循环(cpu100%)。 环形链表(扩容后重新哈希导致)

ConcurrentHashMap  key/value均不能为null

HashMap :先说HashMap,HashMap是线程不安全的,在并发环境下,可能会形成环状链表(扩容时可能造成),导致get操作时,cpu空转,所以,在并发环境中使用HashMap是非常危险的。

HashTable : HashTable和HashMap的实现原理几乎一样,差别无非是1.Hashtable不允许key和value为null;2.Hashtable是线程安全的。

但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

 

 

ConcurrentHashMap的主干是个Segment数组(JDK1.7)

一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组

Segment继承了ReentrantLock,所以它就是一种可重入锁(ReentrantLock)。在ConcurrentHashMap,一个Segment就是一个子哈希表,Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。 以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。

Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel,ssize一定是大于或等于concurrentLevel的最小的2的次幂。比如:默认情况下concurrentLevel是16,则ssize为16;若concurrentLevel为14,ssize为16;若concurrentLevel为17,则ssize为32。

从源码看出,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法

 

ConcurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

ConcurrentHashMap(JDK1.8)    CAS+Synchronized

当有修改操作时借助了synchronized来对table[i]头节点进行锁定保证了线程安全以及使用了CAS来保证原子性操作

sizeCtl是控制标识符

transient volatile Node<K,V>[] table;

容器数组,第一次插入数据的时候初始化,大小是2的幂次方。这就是我们所说的底层结构:”数组+链表(或树)”

ConcurrentHashMap类中相关节点类:Node/TreeNode/TreeBin

Node

       【 node类是table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中】

        Node类是没有提供修改入口的(唯一的setValue方法抛异常),因此只能用于只读遍历。

TreeNode

        链表转树时,并不会直接转,只是把这些节点包装成TreeNode放到TreeBin中,再由TreeBin来转化红黑树。

TreeBin

        当链表转树时,用于封装TreeNode,也就是说,ConcurrentHashMap的红黑树存放的时TreeBin,而不是treeNode。

put(K key, V value)方法

putVal(K key, V value, boolean onlyIfAbsent)方法如下:

1、检查key/value是否为空,如果为空,则抛异常,否则进行2

2、进入for死循环,进行3

3、检查table是否初始化了,如果没有,则调用initTable()进行初始化然后进行 2,否则进行4

4、根据key的hash值计算出其应该在table中储存的位置i,取出table[i]的节点用f表示(头结点)。

根据f的不同有如下三种情况:

1)如果table[i]==null(即该位置的节点为空,没有发生碰撞),则利用CAS操作直接存储在该位置,如果CAS操作成功则退出死循环。

2)如果table[i]!=null(即该位置已经有其它节点,发生碰撞),碰撞处理也有两种情况

        2.1)检查table[i]的节点的hash是否等于MOVED,如果等于,则检测到正在扩容,则帮助其扩容

        2.2)说明table[i]的节点的hash值不等于MOVED[synchronized (f)加锁],如果table[i]为链表节点,则将此节点插入链表中即可;如果table[i]为树节点,则将此节点插入树中即可。插入成功后,进行 5

5、如果table[i]的节点是链表节点,则检查table的第i个位置的链表是否需要转化为树,如果需要则调用treeifyBin函数进行转化

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

get(int key)方法

(阻塞队列)

ArrayBlockingQueue           基于数组的有界阻塞队列

LinkedBlockingQueue         基于链表的有界阻塞队列

PriorityBlockingQueue       支持优先级排序的无界阻塞队列

DelayQueue                         基于优先级队列实现的延时获取元素无界阻塞队列

SynchronousQueue            不存储元素的阻塞队列  (每一个put操作必须等待一个take操作,否则不能继续添加元素)

LinkedTransferQueue         基于链表的无界阻塞队列

LinkedBlockingDeque        基于链表的双向阻塞队列

线程池的使用

Executor 位于最顶层,也是最简单的,就一个 execute(Runnable runnable) 接口方法定义。

ExecutorService 也是接口,在 Executor 接口的基础上添加了很多的接口方法,所以一般来说我们会使用这个接口。

然后再下来一层是 AbstractExecutorService抽象类,这里实现了非常有用的一些方法供子类直接使用, 然后才到我们的重点部分 ThreadPoolExecutor 类,这个类提供了关于线程池所需的非常丰富的功能。

Executors 类--- 工具类, 用于生成 ThreadPoolExecutor 的实例

Java通过Executors提供四种线程池:

        newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

        newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

        newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

        newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO优先级)执行。

1、java 线程池有哪些关键属性?

corePoolSize,maximumPoolSize,workQueue,keepAliveTime,rejectedExecutionHandler

corePoolSize 到 maximumPoolSize 之间的线程会被回收,当然 corePoolSize 的线程也可以通过设置而得到回收(allowCoreThreadTimeOut(true))。

workQueue 用于存放任务,添加任务的时候,如果当前线程数超过了 corePoolSize,那么往该队列中插入任务,线程池中的线程会负责到队列中拉取任务。

keepAliveTime 用于设置空闲时间,如果线程数超出了 corePoolSize,并且有些线程的空闲时间超过了这个值,会执行关闭这些线程的操作

rejectedExecutionHandler 用于处理当线程池不能执行此任务时的情况,默认有抛出 RejectedExecutionException 异常、忽略任务、使用提交任务的线程来执行此任务和将队列中等待最久的任务删除,然后提交此任务这四种策略,默认为抛出异常

2、说说线程池中的线程创建时机?

  1. 如果当前线程数少于 corePoolSize,那么提交任务的时候创建一个新的线程,并由这个线程执行这个任务;

  2. 如果当前线程数已经达到 corePoolSize,那么将提交的任务添加到队列中,等待线程池中的线程去队列中取任务;

  3. 如果队列已满,那么创建新的线程来执行任务,需要保证池中的线程数不会超过 maximumPoolSize,如果此时线程数超过了 maximumPoolSize,那么执行拒绝策略。

* 注意:如果将队列设置为无界队列,那么线程数达到 corePoolSize 后,其实线程数就不会再增长了。

3、Executors.newFixedThreadPool(…) 和 Executors.newCachedThreadPool() 构造出来的线程池有什么差别?

Executors.newFixedThreadPool(…)生成一个固定大小的线程池

最大线程数设置为与核心线程数相等,此时 keepAliveTime 设置为 0(因为这里它是没用的,即使不为 0,线程池默认也不会回收 corePoolSize 内的线程),任务队列采用 LinkedBlockingQueue,无界队列。

过程分析:刚开始,每提交一个任务都创建一个 worker,当 worker 的数量达到 nThreads 后,不再创建新的线程,而是把任务提交到 LinkedBlockingQueue 中,而且之后线程数始终为 nThreads。

Executors.newCachedThreadPool( ) 生成一个需要的时候就创建新的线程,同时可以复用之前创建的线程(如果这个线程当前没有任务)的线程池

核心线程数为 0,最大线程数为 Integer.MAX_VALUE,keepAliveTime 为 60 秒,任务队列采用 SynchronousQueue。

如果线程空闲了 60 秒都没有任务,那么将关闭此线程并从线程池中移除。所以如果线程池空闲了很长时间也不会有问题,因为随着所有的线程都会被关闭,整个线程池不会占用任何的系统资源。

4、任务执行过程中发生异常怎么处理?

如果某个任务执行出现异常,那么执行任务的线程会被关闭,而不是继续接收其他任务。然后会启动一个新的线程来代替它。

5、什么时候会执行拒绝策略?

  1. workers 的数量达到了 corePoolSize(任务此时需要进入任务队列),任务入队成功,与此同时线程池被关闭了,而且关闭线程池并没有将这个任务出队,那么执行拒绝策略。这里说的是非常边界的问题,入队和关闭线程池并发执行,读者仔细看看 execute 方法是怎么进到第一个 reject(command) 里面的。

  2. workers 的数量大于等于 corePoolSize,将任务加入到任务队列,可是队列满了,任务入队失败,那么准备开启新的线程,可是线程数已经达到 maximumPoolSize,那么执行拒绝策略。

线程池

ctl是对线程池的运行状态和线程池中有效线程的数量进行控制的一个字段, 它包含两部分的信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),高3位保存runState,低29位保存workerCount。

runStateOf:获取运行状态;

workerCountOf:获取活动线程数;

ctlOf:获取运行状态和活动线程数的值。

  1. RUNNING :能接受新提交的任务,并且也能处理阻塞队列中的任务;

  2. SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。(finalize() 方法在执行过程中也会调用shutdown()方法进入该状态);

  3. STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态;

  4. TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。

  5. TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。

    进入TERMINATED的条件如下:

    • 线程池不是RUNNING状态;

    • 线程池状态不是TIDYING状态或TERMINATED状态;

    • 如果线程池状态是SHUTDOWN并且workerQueue为空;

    • workerCount为0;

    • 设置TIDYING状态成功。

shutdown方法要将线程池切换到SHUTDOWN状态,并调用interruptIdleWorkers方法请求中断所有空闲的worker,最后调用tryTerminate尝试结束线程池

shutdownNow方法设置线程池状态为STOP,中断所有工作线程,无论是否是空闲的,取出阻塞队列中没有被执行的任务并返回,最后调用tryTerminate方法尝试结束线程池

execute(Runnable command)方法提交任务

在方法内部调用addWorker(Runnable firstTask,Boolean core)方法

addWorker方法

在线程池中创建一个新的线程并执行

firstTask参数用于指定新增的线程执行的第一个任务

core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize

利用Work对象执行任务

Work 类

线程池中的每一个线程被封装成一个Worker对象,ThreadPool维护的其实就是一组Worker对象

Worker类继承了AQS,并实现了Runnable接口

firstTask用它来保存传入的任务;

thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程;

Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。

通过runWorker(this)执行任务:

getTask方法

从execute方法开始,Worker使用ThreadFactory创建新的工作线程,runWorker通过getTask获取任务,然后执行任务,如果getTask返回null,进入processWorkerExit方法,整个线程结束

 

拒绝策略:

1、AbortPolicy    抛出异常(默认)

2、CallerRunsPolicy    用调用者所在线程执行任务

3、DiscardOldestPolicy    丢弃最久任务并提交新任务

4、DiscardPolicy    直接丢弃

常用线程池的应用:

1、newSingleThreadExecutor

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

2、newFixedThreadPool

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

3、newCachedThreadPool

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,

那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务

SynchronousQueue    同步队列

SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。

每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。

因为没有容量,所以对应 peek, contains, clear, isEmpty ... 等方法其实是无效的,例如clear是不执行任何操作的,contains始终返回false,peek始终返回null

SynchronousQueue分为公平和非公平,默认情况下采用非公平性访问策略(公平:TransferQueue    非公平:TransferStack)

创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。对于执行很多短期异步任务的程序而言,这些线程池通常可提高程序性能。

4、newScheduledThreadPool

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

JAVA中的并发工具类

1、等待多线程完成的CountDownLatch

public CountDownLatch(count);  //参数count为计数值

public void await( ) ;   //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行

public boolean await(long timeout, TimeUnit unit);  //和await( )类似,只不过等待一定的时间后count值还没变为0的话就会继续执行

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

2、同步屏障CyclicBarrier

让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。

public CyclicBarrier(int parties, Runnable barrierAction);  

public CyclicBarrier(int parties);

参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。

public int await( );

public int await(long timeout, TimeUnit unit);

第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;

第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。

CountDownLatch的计数器只能使用一次,CyclicBarrier的计数器可以重用。

3、控制并发线程数的Semaphore  (信号量)

用来控制同时访问特定资源的线程数量,它通过协调各个线程以保证合理的使用公共资源。

通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

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

    sync = new NonfairSync(permits);

}

public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可

    sync = (fair)? new FairSync(permits) : new NonfairSync(permits);

public void acquire( )     //获取一个许可

public void acquire(int permits)  //获取permits个许可

public void release( )          //释放一个许可

public void release(int permits)    //释放permits个许可

acquire()用来获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。

release()用来释放许可。注意,在释放许可之前,必须先获获得许可。

public boolean tryAcquire( )    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false

public boolean tryAcquire(long timeout, TimeUnit unit) //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返 回false

public boolean tryAcquire(int permits) //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false

public boolean tryAcquire(int permits, long timeout, TimeUnit unit)  //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

三个工具类的比较:

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

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

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

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

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

4、线程间交换数据的Exchanger

用于线程间协作的工具类,它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。

当线程A调用Exchange对象的exchange()方法后,他会陷入阻塞状态,

直到线程B也调用了exchange()方法,然后以线程安全的方式交换数据,之后线程A和B继续运行。

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值