JUC多线程-2

并发工具类

CyclicBarrier

同步屏障,只有到达这个屏障的线程到达指定数目时,所有线程才能继续运行下去

在屏障前阻塞后,只有符合以下情况才能结束等待:

  1. 最后一个线程到达,即index == 0
  2. 超出了指定时间(超时等待)
  3. 其他的某个线程中断当前线程
  4. 其他线程中断了另一个等待的线程
  5. 其他的某个线程在等待屏障超时
  6. 其他某个线程调用了屏障的reset()方法,恢复初始状态

构造方法

CyclicBarrier(Integer parties);
CyclicBarrier(Integer parties, Runnable barrierAction);

第一个构造方法是调用第二个构造方法的

当到达屏障的线程数达到要求时,最后一个到达的线程会运行barrierAction

调用的方法是:

CyclicBarrier barrier = new CyclicBarrier(5);
barrier.await();

CountDownLatch

一个计数的闭锁,作用类似于CyclicBarrier。

用给定的计数初始化CountDownLatch,由于调用了await()方法,所以在当前计数通过countDown()方法到达零之前,await方法会一直受到阻塞。之后,会释放所有等待的线程。
这种现象只出现一次——计数无法被重置。如果需要重置计数,请考虑使用CyclicBarrier

· CountDownLatch:一个或多个线程,等待其他多个线程完成某件事情之后才能执行;
· CyclicBarrier: 多个线程互相等待,直到到达同一个同步点(屏障),再继续一起执行

两者的区别还在于,CountDownLatch 线程等待的是其他线程调用countDown方法;而CyclicBarrier 线程等待的是其他和自己一样等待的线程

Semaphore

控制访问多个共享资源的计数器。

Semaphore维护了一个信号量许可集。线程可以获取信号量的许可。如果信号量中有可用的许可,则可以继续运行,否则必须等待,直到有许可为止。

类似停车场停车位。有如果没有空位了,其他车就只能等到其他车离开,才能停车。

Semaphore常用语约束一些(物理或逻辑)资源的线程数量

当信号量初始化为1时,就相当于是互斥锁。

并发容器ConcurrentHashMap

HashMap线程不安全,而HashTable效率又太低

JDK7时,ConcurrentHashMap采用了分段所的概念。
JDK8时,改成了CAS+Synchronized来保证并发更新的安全。
当然底层还是数组+链表+红黑树的结构

JDK7版本的ConcurrentHashMap

由Segment数组组成,初始化时可以设置Setment数,默认为16,初始化后不可扩容。相当于每个Segment都是一个HashTable。每次锁住的都是一个Segment。所以叫分段锁。

JDK7时,HashMap由数组+链表组成;而JDK8时,HashMap由数组+链表+红黑树组成

区别在于,JDK8时,在链表长度超过8时,会将链表转换为红黑树,小于6时,又会将红黑树转回链表。

队列

非阻塞队列ConcurrentLinkedQueue

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

注意:

  1. ConcurrentLinkedQueue的size()方法是要遍历一遍集合的,所以要尽可能避免使用
  2. 使用了这个类之后,还是需要自己进行同步或者加锁操作。因为线程安全只能保证自己的一个操作,而无法对多次操作保证线程安全。因为多次操作无法保证操作原子性

阻塞队列BlockingQueue

有两种情况会产生阻塞:

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

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

  1. 抛出异常
  2. 返回特殊值
  3. 阻塞等待,直到操作成功
  4. 阻塞等待,直到成功或者超时

操作 抛出异常 特殊值 阻塞 超时
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time,unit)
检查 element() peek() 不可用 不可用

BlockingQueue是接口,接下来介绍它的几个实现类

ArrayBlockingQueue

一个由数组实现的有界阻塞队列。有界且固定,其大小在构造时由构造函数决定

支持对等待的生产者线程和使用者线程进行排序。可以选择公平或者不公平策略

LinkedBlockingQueue

和ArrayBlockingQueue使用方式基本一致

区别:

ArrayBlockingQueue是一个由数组支持的有界阻塞队列
队列中的锁是没有分离的,即生产和消费用的是同一个锁
生产或消费时,直接将对象插入或移除
必须指定大小

LinkedBlockQueue是一个由链表支持的队列(可设置)阻塞队列
锁是分离的,生产用的是putLock,消费用的是takeLock
需要将对象转换成Node,再插入或移除
不需要指定大小

PriorityBlockingQueue

优先级队列。在java.util.PriorityQueue基础上提供了可阻塞的读取操作

特点:

  1. 优先队列不允许空值,而且不支持不可比较的对象。因为内部需要使用Comparable或Comparator接口给对象排序。
  2. 优先队列的头饰基于自然排序或者Comparator排序的最小元素。如果有多个最小的,则则会随机选择。也可以通过提供的Comparator(比较器)在队列中实现自定义排序。
  3. 大小不受限制,但是创建时可以指定初始大小。如果增加元素时容量不够,会自动增加。
  4. 基于PriorityQueue,是用于多线程环境的。

SynchronousQueue

实际上不是一个真正的队列,因为它不存储元素。它维护一组线程,这些线程在等待着添加或移除元素。它永远阻塞着,直到有线程要put并且有线程要take时,才会将元素转交一下。

线程池

线程是一个程序员一定会涉及到的概念,但是线程的创建和切换的代价都是比较大的所以我们需要一个比较好的方案能够做到线程的复用。这里就涉及到一个概念:线程池。河里使用线程池有三个明显的好处:

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

主要通过ThreadPoolExecutor来完成

变量ctl定义为AtomicInteger, 记录了“线程池中的任务数量”和“线程池的状态”两个信息。工32位。其中高3位表示线程池状态,低29位表示线程池中的任务数量

线程池状态:

  1. RUNNING:能够接受新任务,以及对新添加的任务进行处理
  2. SHUTDOWN:不能接受新任务,但是会对已添加的任务进行处理
  3. STOP:不接受新任务,不处理新添加的任务,并且终端正在处理的任务
  4. TIDYING:当所有任务已终止,ctl记录的任务书为0,则会编程这个状态。会执行钩子函数terminated(相当于finallize的用法)
  5. TERMINATED:线程池彻底终止的状态。

构造方法参数:

corePoolSize:核心线程数量
maximumPoolSize:允许的最大线程数量。只有选择的阻塞队列有界才有效
keepAliveTime:线程空闲时间。只有线程数大于corePoolSize才生效
unit:时间的单位
workQueue:阻塞队列
threadFactory:创建线程的工厂。可以设置线程的名字。默认都是非守护线程,线程优先级也是默认的。
handler:线程的拒绝策略:就是线程满了,而且队列也满了,线程池会选择一种拒绝策略来处理
	AbortPolicy:直接抛出异常,默认策略
	CallerRunsPolicy:用调用者所在的线程来执行任务
	DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并执行当前任务
	DiscardPolicy:直接丢弃任务
	当然也可以实现自己的拒绝策略,例如记录日志等。实现RejectedExecutionHandler接口即可

FixedThreadPool

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

其定义如下:

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

最大线程数和核心线程数都设置成nThreads了。不会创建新的线程,而是都放在阻塞队列中。

执行方式:

ExecutorService exec = Executors.newFixedThreadPool(5);
exec.execute(new Runnabe(){
	public void run(){}
});

SingleThreadExecutor

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

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

只会有一个线程,如果这个线程崩了,就重启一个线程继续工作。

CachedThreadPool

会根据需要,在线程可用时,会重用之前构造线程

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

核心线程为0,最大线程为Integer.MAX_VALUE,意味着所有线程一来就会加入到阻塞队列中。因为线程池的基本大小为0,所以一般情况下线程池中没有空闲的线程。

阻塞队列采用的是SynchronousQueue,这是一个没有元素的阻塞队列

这种线程池,用于执行大量短生命周期的异步任务。在60s内,线程被销毁前,可以多次重用。当任务高峰期结束后,则会自动销毁。

要注意控制并发的任务数,避免创建过多的线程,耗尽系统内存

ScheduledThreadPoolExecutor

继承ThreadPoolExecutor,且实现了ScheduledExecutorService。相当于提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);// 核心线程数

// 五秒后执行
scheduledThreadPool.schedule(new Runnable(){},5,TimeUnit.SECONDS);

// 关闭线程池
scheduledThreadPool.shutdown();

你可能感兴趣的:(#,并发包)