JUC多线程基础(三)

1. 并发工具类

1.1 CyclicBarrier

1.1.1 CyclicBarrier简介

CyclicBarrier是同步屏障,它的作用类似于一道门,默认情况下关闭,堵住所以线程的道路,直到所有线程就位,让所有线程一起通过。

1.1.2 实现

在这里插入图片描述
内部使用了ReentrantLockCondition

1.1.3 使用

构造方法:

  1. CyclicBarrier(int parties):它将在给定数量的参与者(线程)处于等待状态时启动。parties表示拦截线程的数量
  2. CyclicBarrier(int parties, Runnable barrierAction):创建一个新的 CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动屏障时执行给定的屏障操作,该操作由最后一个进入屏障的线程执行。

阻塞方法:await()

1.1.4 注意

n个线程放行时,又会继续拦截一批

1.2 CountDownLatch

1.2.1 CountDownLatch简介

CountDownLatch时一个计数的闭锁,让一个线程等待其他多个线程完成某件事之后才能执行。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后就可以恢复等待的线程继续执行了

在这里插入图片描述

1.2.2 实现

在这里插入图片描述
CountDownLatch内部依赖Sync实现,而Sync继承AQS,CountDownLatch是采用共享锁来实现的。

1.2.3 使用

  1. await方法:使当前线程在锁存器倒计数至零之前一直等待,除非线程被中断。内部使用AQS的getState方法获取计数器,如果计数器值不等于0,则会以自旋方式会尝试一直去获取同步状态。
  2. countDown方法:递减锁存器的计数,如果计数到达零,则释放所有等待的线程。内部调用AQS的releaseShared(int arg)方法来释放共享锁同步状态。

1.3 Semaphore

1.3.1 Semaphore简介

Semaphore是一个控制访问多个共享资源的计数器,和CountDownLatch一样,其本质上是一个“共享锁”。

Semaphore维护了一个信号量许可集。线程可以获取信号量的许可;当信号量中有可用的许可时,线程能获取该许可;否则线程必须等待,直到有可用的许可为止。 线程可以释放它所持有的信号量许可,被释放的许可归还到许可集中,可以被其他线程再次获取。

当信号量为1时可以当作互斥锁使用

1.3.2 实现

在这里插入图片描述
Semaphore内部包含公平锁(FairSync)和非公平锁(NonfairSync),继承内部类Sync,其中Sync继承AQS

1.3.3 使用

构造方法:

  1. Semaphore(int permits):创建具有给定的许可数和非公平的 Semaphore。
  2. Semaphore(int permits, boolean fair):创建具有给定的许可数和给定的公平设置的 Semaphore。

获取许可:acquire()方法
信号量释放:release()方法

2. 并发容器

2.1 ConcurrentHashMap

2.1.1 ConcurrentHashMap简介

HashMap是一个使用非常频繁的容器,但是它是线程不安全的。在jdk8之前put操作甚至会产生死循环。

解决该问题的方案可以使用:

  1. HashTable
  2. Collections.synchronizedMap

但是效率过低,他们的方法都是对读写加锁。
所以我们采用效率较高的ConcurrentHashMap


jdk8之前,ConcurrentHashMap使用的是分段锁的概念,使锁细化。

jdk8之后,利用了CAS + Synchronized保证并发更新安全。


2.1.2 JDK7的HashMap

在这里插入图片描述
jdk7HashMap实现是数组 + 链表,绿色格子代表Entry实例。

构造方法:public HashMap(int initialCapacity, float loadFactor)

capacity:当前数组容量,始终是2^n,每次扩容是当前数组的2倍
loadFactor:负载因子,默认为0.75
threshold:扩容的阈值,等于capacity * loadFactor


put过程:

  1. 数组初始化,在第一个元素插入HashMap时做初始化,先确定了初始数组大小
  2. 计算具体数组位置,根据key进行hash运算
  3. 找到下标后判断是是否key重复,如果没重复放在表头
  4. 在插入新值之前,如果size达到了阈值,并且当前插入的数组位置上已经有了元素,就会触发扩容。

get过程:

  1. 根据key算出hash
  2. 根据hash找到对应的数组下标
  3. 遍历该位置的链表,找到相等的key

2.1.3 JDK7的ConcurrentHashMap

ConcurrentHashMap由一个个的Segment组成,代表一段,所以又叫分段锁。
ConcurrentHashMap是一个Segment数组,Segment通过继承ReentrantLock进行加锁,每次加锁锁住的是一个Segment,这样保证每个Segment是安全的。
在这里插入图片描述
Segment内部和之前的HashMap就很类似了。


初始化:public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)

initialCapacity:整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment
concurrencyLevel:并发数(或者Segment 数,有很多叫法,重要的是如何理解)。默认是 16,也就是说 ConcurrentHashMap 有 16 个 Segments,所以这个时候,最多可以同时支持 16 个线程并发写,只要它们的操作分别分布在不同的 Segment 上。这个值可以在初始化的时候设置为其他值,但是一旦初始化以后,它是不可以扩容的
loadFactor:负载因子,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的


put过程:

  1. 根据hash值找到对应的Segment
  2. 获取该Segment的独占锁
  3. 用上面的HashMap的方法在找到对应的位置

get过程:

  1. 计算hash值,找到Segment的对应位置
  2. Segment中找到具体的数组位置
  3. 顺着链表查找

2.1.4 JDK8的HashMap

jdk8利用了红黑树,所以组成有:数组 + 链表 + 红黑树
在链表的长度超过了8之后,会将链表转化为红黑树。
在这里插入图片描述
jdk7中使用Entry代表数据节点,jdk8使用Node,当改成红黑树之后会使用TreeNode

put过程:jdk8之后是先插值在扩容
get过程:

  1. 计算hash值,找到对应的下标
  2. 判断该位置是否是要找的元素
  3. 如果是TreeNode类型用红黑树方法去找,否则遍历链表

2.1.5 注意

在并发执行时,线程安全的容器只能保证自身的数据不被破坏,和数据在多个线程间是可见的,但无法保证业务的行为是否正确。

2.2 HashTable

Hashtable和ConcurrentHashMap的不同点:

  1. Hashtable对get,put,remove都使用了同步操作,它的同步级别是正对Hashtable来进行同步的,也就是说如果有线程正在遍历集合,其他的线程就暂时不能使用该集合了,这样无疑就很容易对性能和吞吐量造成影响,从而形成单点。而ConcurrentHashMap则不同,它只对put,remove操作使用了同步操作,get操作并不影响。
  2. Hashtable在遍历的时候,如果其他线程,包括本线程对Hashtable进行了put,remove等更新操作的话,就会抛出ConcurrentModificationException异常,但如果使用ConcurrentHashMap的话,就不用考虑这方面的问题了

2.3 ConcurrentSkipListMap

Skip List ,称之为跳表,它是一种可以替代平衡树的数据结构,其数据元素默认按照key值升序,天然有序。Skip list让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过“空间来换取时间”的一个算法,在每个节点中增加了向前的指针,在插入、删除、查找时可以忽略一些不可能涉及到的结点,从而提高了效率。
在这里插入图片描述
上面就是一个典型的跳表

3. 队列

要实现一个线程安全的队列有两种方式:阻塞和非阻塞

queue 是否阻塞 是否有界 线程安全保证
ConcurrentLinkedQueue 非阻塞 无界 CAS
ArrayBlockingQueue 阻塞 有界 全局锁
LinkedBlockingQueue 阻塞 可配置 存取采用两把锁
PriorityBlockingQueue 阻塞 无界 全局锁
SynchronousQueue 阻塞 无界 CAS

3.1 ConcurrentLinkedQueue

ConcurrentLinkedQueue是一个基于链接节点的无边界的线程安全队列,遵循队列的FIFO原则,队尾入队,队首出队。采用CAS算法来实现的。

@Test
public void test() {
    
    
	Queue<Object> queue = new ConcurrentLinkedQueue<>();
	// 添加
	queue.add("...");
	// 弹出并获取返回值
	queue.poll();
	// 判断是否为空
	queue.isEmpty();
	// 得到元素个数,注意!!会遍历整个队列
	queue.size();
}

3.2 阻塞队列BlockingQueue

被阻塞的情况主要有如下两种:

  1. 当队列满了的时候进行入队列操作
  2. 当队列空了的时候进行出队列操作

BlockingQueue 对插入操作、移除操作、获取元素操作提供了四种不同的方法用于不同的场景中使用:

  1. 抛出异常
  2. 返回特殊值
  3. 阻塞等待直到成功
  4. 阻塞等待知道成功或者超时
操作 抛出异常 特殊值 阻塞 超时
插入 add(e) offfer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek()

3.3 ArrayBlockingQueue

ArrayBlockingQueue是一个由数组实现的有界阻塞队列。该队列采用FIFO的原则对元素进行排序添加的。

ArrayBlockingQueue为有界且固定,其大小在构造时由构造函数来决定,确认之后就不能再改变了。

ArrayBlockingQueue支持对等待的生产者线程和使用者线程进行排序的可选公平策略,但是在默认情况下不保证线程公平的访问,在构造时可以选择公平策略(fair = true)。公平性通常会降低吞吐量,但是减少了可变性和避免了“不平衡性”。

ArrayBlockingQueue内部使用可重入锁ReentrantLock + Condition来完成多线程环境的并发操作:

  1. items:定长数组,维护元素
  2. takeIndex:队首位置
  3. putIndex:队尾位置
  4. count:元素个数
  5. lock:锁,出入队都要先获取

3.4 LinkedBlockingQueue

  1. LinkedBlockingQueue是一个基于链表的有界(可设置)阻塞队列
  2. LinkedBlockingQueue实现的队列中的锁是分离的,即生产用的是putLock,消费是takeLock
  3. LinkedBlockingQueue实现的队列中在生产和消费的时候,需要把枚举对象转换为Node进行插入或移除,会影响性能
  4. LinkedBlockingQueue实现的队列中可以不指定队列的大小,但是默认是Integer.MAX_VALUE
  5. 使用方法基本同ArrayBlockingQueue

3.5 PriorityBlockingQueue

PriorityBlockingQueue始终保证出队的元素是优先级最高的元素,并且可以定制优先级的规则,内部使用二叉堆,通过使用一个二叉树最小堆算法来维护内部数组,这个数组是可扩容的,当前元素个数>=最大容量时候会通过算法扩容。值得注意的是为了避免在扩容操作时候其他线程不能进行出队操作,实现上使用了先释放锁,然后通过CAS保证同时只有一个线程可以扩容成功。

3.6 SynchronousQueue

其实并不是一个真正的队列,因为他并不会维护存储空间。
它维护一组线程,这些线程等待着把元素加入或者移除队列。
putget会一直阻塞,知道另一个线程做出另一个操作。

  1. iterator(): 永远返回空,因为里面没东西。
  2. peek() :永远返回null。
  3. put() :往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
  4. offer():往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
  5. offer(2000, TimeUnit.SECONDS) :往queue里放一个element但等待时间后才返回,和offer()方法一样。
  6. take():取出并且remove掉queue里的element,取不到东西他会一直等。
  7. poll() :取出并且remove掉queue里的element,方法立即能取到东西返回。否则立即返回null。
  8. poll(2000, TimeUnit.SECONDS) :等待时间后再取,并且remove掉queue里的element,
  9. isEmpty():永远是true。
  10. remainingCapacity() :永远是0。
  11. remove()和removeAll() :永远是false。

4. 线程池

线程池的好处:

  1. 降低资源消耗:通过重用已经创建的线程来降低线程创建和销毁的消耗
  2. 提高响应速度:任务到达时不需要等待线程创建就可以立即执行。
  3. 提高线程的可管理性:线程池可以统一管理、分配、调优和监控。

java的线程池支持主要通过ThreadPoolExecutor来实现,我们使用的ExecutorService的各种线程池策略都是基于ThreadPoolExecutor实现的,所以ThreadPoolExecutor十分重要。要弄明白各种线程池策略,必须先弄明白ThreadPoolExecutor。

4.1 线程池的状态

有五种状态:

  1. Running
  2. SHUTDOWN
  3. STOP
  4. TIDYING
  5. TERMINATED

在这里插入图片描述

变量ctl记录了线程池的任务数量和状态

  1. 高三位表示线程池状态
  2. 低29位表示任务池中的任务数量

在这里插入图片描述
状态转化如图

4.2 构造方法

 public ThreadPoolExecutor(int corePoolSize, 
 						   int maximumPoolSize,
 						   long keepAliveTime, 
 						   TimeUnit unit, 
 						   BlockingQueue<Runnable> workQueue, 						
 						   ThreadFactory threadFactory, 
 						   RejectedExecutionHandler handler) {
    
    
        this.ctl = new AtomicInteger(ctlOf(-536870912, 0));
        this.mainLock = new ReentrantLock();
        this.workers = new HashSet();
        this.termination = this.mainLock.newCondition();
        if (corePoolSize >= 0 && maximumPoolSize > 0 && maximumPoolSize >= corePoolSize && keepAliveTime >= 0L) {
    
    
            if (workQueue != null && threadFactory != null && handler != null) {
    
    
                this.corePoolSize = corePoolSize;
                this.maximumPoolSize = maximumPoolSize;
                this.workQueue = workQueue;
                this.keepAliveTime = unit.toNanos(keepAliveTime);
                this.threadFactory = threadFactory;
                this.handler = handler;
            } else {
    
    
                throw new NullPointerException();
            }
        } else {
    
    
            throw new IllegalArgumentException();
        }
}

参数含义:

  1. corePoolSize:核心线程的数量,当提交一个任务时,线程池会创建一个线程来执行任务,不管是否有空间的线程,直到线程数等于corePoolSize为止,刚创建时线程池为空的。

  2. maximumPoolSize:允许的最大线程数,如果阻塞队列满了,还有任务提交并且当前线程数小于maximumPoolSize,会创建新线程来执行任务。

  3. keepAliveTime:线程空闲时间,线程执行完任务后不会立即销毁,默认情况下,只有当前线程数大于corePoolSize才有效。

  4. unitkeepAliveTime的单位

  5. workQueue:保存等待执行的任务的阻塞队列,等待的任务必须实现Runnable接口。

    ArrayBlockingQueue
    LinkedBlockingQueue
    PriorityBlockingQueue

  6. threadFactory:用来创建线程的工厂。

  7. handler:拒绝策略,将任务添加到线程池中,拒绝该任务所采取的策略(如果线程饱和并且阻塞队列也满了,会拒绝)

    AbortPolicy: 直接抛出异常(默认)
    CallerRunsPolicy:调用者所在的线程来执行
    DiscardPolicy:丢弃该任务
    DiscardOldestPolicy:丢弃最考前的任务
    自己实现:实现RejectedExecutionHandler接口

4.3 四种线程池

Executor框架提供了三种线程池,他们都可以通过工具类Executors来创建。

还有一种线程池ScheduledThreadPoolExecutor,它相当于提供了“延迟”和“周期执行”功能的ThreadPoolExecutor

4.3.1 FixedThreadPool

复用固定数量的线程处理一个共享的无边界队列

public static ExecutorService newFixedThreadPool(int nThreads) {
    
    
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

核心线程数和最大线程数相同,所以如果没空闲线程,就直接添加到阻塞队列中。

// 无参默认整数最大值
ExecutorService exec = Executors.newFixedThreadPool(3);
exec.execute(实现的Runnable);

4.3.2 SingleThreadExecutor

SingleThreadExecutor只会使用单个工作线程来执行一个无边界的队列。

public static ExecutorService newSingleThreadExecutor() {
    
    
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

4.3.3 CachedThreadPool

CachedThreadPool会根据需要,在线程可用时,重用创建好的线程,否则创建新线程。

public static ExecutorService newCachedThreadPool() {
    
    
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

注意初始线程数为0,允许空闲时间为一分钟。
适用于短时间内执行多次任务,但是不能特别大,否则相当于创建无穷个线程,CPU扛不住。

4.3.4 ScheduledThreadPool

TimerTimerTask可以实现线程的周期和延迟调度,但是存在问题,推荐采用ScheduledThreadPool来解决。

猜你喜欢

转载自blog.csdn.net/weixin_43795939/article/details/112791568