概述
1.是JDK1.5提供的应对高并发的基础包
2.主要包含:BlockingQueue,ConcurrentMap,ExecutorService,Lock,原子性操作
BlockingQueue-阻塞式队列
本质是队列,满足队列的原则(先进先出FIFO)。
所有的阻塞式队列都是有界的-当队列定义好之后,大小就不可变。
阻塞:当队列已满的时候,再试图放入的线程会被阻塞。当队列为空的时候,再试图拿支的线程会被阻塞。
要求队列中的元素必须非空
方法
抛出异常 | 返回值 | 阻塞 | 定时阻塞 | |
添加 | add | offer-true/false | put | offer |
获取 | remove | poll-null | take | poll |
ArrayBlockingQueue-阻塞式顺序队列
底层是基于数组进行存储
使用的时候需要指定容量,定义好后容量不可变
LinkedBlockingQueue-并发队列
底层是基于节点实现的
在使用的时候可以不指定容量。如果指定了容量,指定多大就是多大。如果不指定容量,默认容量是Integer.MAX_VALUE=>2的31次方-1.因为在实际生产环境中,一般不会向一个队列中添加21亿个值,所以一般会认为这个队列如果不指定容量就是无界的。
PriorityBlockingQueue-具有优先级的阻塞式队列
在使用的时候可以指定容量,也可以不指定。如果不指定容量,默认容量是11.(DEFAULT_INITIAL_CAPACITY = 11)
在拿取元素的时候,会对元素进行排序(自然排序-自然排序如果不指定一般是升序)。
要求元素所对应的类必须实现Comparable接口,重写compareTo方
如果使用迭代遍历的方式,则此时排序规则无效。
SynchronousQueue-同步队列
使用的时候不需要指定容量,默认容量为1
BlockingDeque-阻塞式双向队列
允许两端添加或者获取元素
继承了BlockingQueue
ConcurrentMap-并发映射
本质上是一个Map,存储的元素依然是键值对结构
保证映射的并发能力以及线程安全的能力。
ConcurrentHashMap-并发哈希映射
底层是数组+链表结构来存储数据。
默认初始容量是16,默认加载因子是0.75,扩容的时候,默认每次增加一倍。
采用了分段(分桶)锁机制。在后续版本中,ConcurrentHashMap为了提高效率,在分段锁的基础上,引入了读写锁机制。
a.读锁:允许多个线程读,不允许线程写。
b.写锁:只允许一个线程写。不允许线程读。
在JDK1.8中,引入了CAS(Compare And Swap-比较和交换)无锁算法。保证线程安全性。
CAS:我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少。(V:内存值,A:旧的预期值,B:新的预期值。CAS过程只要被打断,那么就会从头重新比较)。
从JDK1.8开始,如果一个桶中的元素个数超过了8个的时候,这个桶中的链表会扭转成一棵红黑树;如果不足8个,那么红黑树扭转回链表。
a.红黑树:红黑树本质上是一棵自平衡二叉查找树。
b.二叉查找树:左子树都小于根,右子树都大于根。
c.红黑树的特点:
i所有的节点非红即黑
ii根节点是黑节点
iii红节点的子节点一定是黑节点,但是黑节点的子节点不一定是红节点。
iv最底层的叶子节点一定是黑色的空节点。
v从根节点到任意一个叶子节点所经过的路径中的黑色节点个数一致的。即黑节点是高度一致的。
vi新添的节点一定是红结点
红黑树的修改:前提一定是父子节点都为红:
i叔父节点为红,那么将父节点涂黑,将督父节点涂黑,将祖父节点涂红。
ii叔父节点为黑,并且当前节点为右子叶,则以当前节点为轴进行左旋
叔父节点为黑,并且当前节点为左子叶时。以父节点为轴,右旋
根节点为黑,右侧黑节点多,改为红。
修正案例:
红黑树的查询时间复杂度是O(logn)
红黑树可以认为是二分查找的空间结构。
ConcurrentNavigableMap-并发导航映射
提供了截取子映射的方法
一般使用的是实现类ConcurrentSkipListMap-并发跳跃表映射
跳跃表:
a.要求元素必须有序
b.针对原来的数据进行提取形成跳跃表,跳跃表可以向上一层层提 取,但是最顶层的跳跃表中的元素至少有2个,一般是2-4个。
c.跳跃表是典型的以空间换时间的产物。
d.适用于查询多而增删少的场景
e.如果新添加节点,这个节点是否提取到上一层跳跃表中,遵循“抛硬币”原则。
f.跳跃表的查询时间复杂度是O(logn)
ExecutorService-执行器服务
线程池的意义:减少服务器端的线程的创建和销毁,做到线程的重用。
线程池在创建的时候需要定义一定数量的线程。
每接受一个请求都会创建一个线程(核心线程)去处理这个请求。
核心线程在处理完请求之后不会被销毁,而是等待下一个请求。
在核心线程达到数量之前,每次过来的请求都会去创建一个新的核心线程。
如果所有的核心线程都被占用,那么后续的请求会被放入工作队列中。工作队列本质上是一个阻塞式队列(有界)。
如果工作队列也被全部占用,则线程池会创建一个临时线程去处理这个请求。如果临时线程执行完成之后,会存活指定的一段时间,如果这段时间内没有新的任务处理,则这个临时线程才会被kill掉。
如果临时线程也被全部占用,则新来的请求会交给 RejectedExecutionHandler(拒绝执行处理器处理)处理
工作队列中的请求只会交给核心线程
public class ExecutorServiceDemo2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
/*特点
1没有核心线程全部都是临时线程
2临时线程的数量是Integer.MAX_VALUE,可以认为这个线程池中包含的线程数量是无限的
3临时线程用完之后允许存活1min
4工作队列是一个同步队列
大池子小队列
适用场景:高并发的短任务场景。
*/
ExecutorService es= Executors.newCachedThreadPool();
/*
特点:
1没有临时线程,全部都是核心线程
2工作队列是一个阻塞式链式队列,默认容量是Integer.MAX_VALUE.可以存储无限多个任务
小池大队列
适用场景:长任务。
*/
ExecutorService es2= Executors.newFixedThreadPool(5);
Future<String> f= es.submit(new CThread());
System.out.println(f.get());
}
}
class CThread implements Callable<String>{
@Override
public String call() throws Exception {
return "ok";
}
}
Callable
是JDK1.5提供的一个并发和返回结果的线程
Callable和Runable的区别
a.返回值:Runnable无返回值,Callable可以定义返回值
b.启动方法:Runnable可以通过Thread类或者是线程池来启动,但是Callable只能通过线程池启动
c.异常机制:Runnable不允许抛出异常,Callable允许抛出异常,这就意味着Callable如果报错可以利用全局方式处理。
ScheduledExecutorService -定时执行器服
可以实现定时效果
是很多定时器的底层机制
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
//创建定时线程池
ScheduledExecutorService ses = Executors.newScheduledThreadPool(5);
//定时执行
ses.schedule(new ScheduleRunnable(), 5, TimeUnit.SECONDS);//5秒后执行
ses.scheduleAtFixedRate(new ScheduleRunnable(), 0, 5, TimeUnit.SECONDS);//每5秒执行一次
ses.scheduleWithFixedDelay(new ScheduleRunnable(), 0, 5, TimeUnit.SECONDS);//每5秒执行一次//执行完之后计时
}
}
class ScheduleRunnable implements Runnable {
@Override
public void run() {
Calendar now = Calendar.getInstance();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("hello:"+"秒: " + now.get(Calendar.SECOND));
}
}
Fork
JoinPool-分叉合并池
分叉:将一个大任务拆分成多个小任务然后分布在不同的核上执行。
合并:将分叉出去的线程的执行结果进行汇总。
分叉合并能够有效的提高CPU的利用率。
数据量越大,分叉合并相对循环而言的效率就越高。
分叉合并在分配的时候,会根据核上已有的线程数量来进行尽量均衡的分配。
为了防止满任务导致整体效率降低,分叉合并采取了work-stealing(工作窃取)策略-当一个核上的任务执行完成之后,这个核不会空闲而是去随机扫描一个核 然后 从这个核的任务列表的尾端来“偷”一个任务回来执行。
Lock-锁
synchronized在使用的时候,需要关注锁对象。它的使用不够灵活。
Lock是锁的顶级接口,所以使用的是实现类ReentrantLock
ReadWriteLock:读写锁。
a.读锁:允许多个线程读,但是不允许线程写。
b.写锁:只允许一个线程写,但是不允许线程读。
公平和非公平策略
a.非公平策略:在资源有限的情况下,有可能出现线程之间抢占的次数不均等的情况
b.公平策略:在资源有限的情况下,保证每一个线程执行的次数是基本均等的。
c.非公平策略的效率更高
d.synchronized是非公平的策略
e.Lock默认也是非公平的。
其它锁
CountDownLatch-闭锁/线程递减锁
对线程进行计数,当计数归零的时候放开阻塞。
package com.yasuofenglei.lock;
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl=new CountDownLatch(5);
new Thread(new Teacher(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
new Thread(new Student(cdl)).start();
cdl.await();
System.out.println("考试开始");
}
}
class Teacher implements Runnable {
private CountDownLatch cdl;
public Teacher(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
System.out.println("监考老师到达考场~~");
cdl.countDown();
}
}
class Student implements Runnable {
public Student(CountDownLatch cdl) {
this.cdl = cdl;
}
private CountDownLatch cdl;
@Override
public void run() {
System.out.println("考生到达考场~~");
cdl.countDown();
}
}
CyclicBarrier-栅栏
对线程进行计数,当计数归零的时候开放阻塞
Exchanger-交换机
用于交换两个线程之间的信息
package com.yasuofenglei.lock;
import java.util.concurrent.Exchanger;
public class ExchangerDemo {
public static void main(String[] args) {
Exchanger<String> ex=new Exchanger<>();
new Thread(new Producer(ex)).start();
new Thread(new Consumer(ex)).start();
}
}
class Producer implements Runnable{
private Exchanger<String> ex;
public Producer(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
String info="商品";
//生产者应该将准备的商品交换给消费者
try {
String msg=ex.exchange(info);
System.out.println("生产者收到了消费者交换的:"+msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
private Exchanger<String> ex;
public Consumer(Exchanger<String> ex) {
this.ex = ex;
}
@Override
public void run() {
String info="钱";
try {
String msg=ex.exchange(info);
System.out.println("消费者收费生产者:"+msg);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Semaphore-信号量
每一个线程可以取得一个信饕量,当信号量被全部占用完之后,后来的线程就会被阻塞,直到有信号量被释放,那么阻塞的线程才能获取信号量继续执行。
实际开发中可以使用信号量来实现限流。
package com.yasuofenglei.lock;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
Semaphore s = new Semaphore(5);
for (int i = 0; i < 8; i++) {
new Thread(new Table(s)).start();
}
}
}
class Table implements Runnable {
private Semaphore s;
public Table(Semaphore s) {
this.s = s;
}
@Override
public void run() {
try {
//桌子每被占且张,信号量应该是减少一个
//当信号量减为0,那么后来的线程应该被阻塞
s.acquire();
System.out.println("来了一拨人。占用一桌");
Thread.sleep((long) Math.random() * 1000);
System.out.println("空出一桌");
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
原子性操作
这个属性在计算过程中,不会被其他线程抢占,保证属性的原子性。
实际开发中,可以使用的锁机制来代替。
package com.yasuofenglei.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicIntegerDemo {
// static int i = 0;
//无子性整数,可以认为是对单个属性的锁机制
static AtomicInteger ai = new AtomicInteger(0);
public static void main(String[] args) throws Exception{
new Thread(new Add2()).start();
new Thread(new Add2()).start();
Thread.sleep(1000);
System.out.println(ai.get());
}
}
class Add2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
AtomicIntegerDemo.ai.incrementAndGet();
}
}
}