JUC并发编程(基础)
一 JUC
1.1 进程和线程
-
进程: 指在系统中正在运行的一个应用程序; 程序一旦运行就是进程; 进程是资源分配的最小单位
-
线程: 系统分配处理器时间资源的基本单位,或者说是进程之内独立执行的一个单元执行流. 线程程序执行的最小单位
1.2 线程的状态
1.2.1 线程状态枚举类
-
new (新建)
-
runnable(就绪)
-
blocked(阻塞)
-
waiting(不见不散)
-
timed_waiting(过时不候)
1.2.2 wait/sleep的区别
-
sleep是Thread的静态方法, wait是Object的方法, 任何对象实例都能调用
-
sleep不会释放锁, 不需要占用锁 ; wait会释放锁, 前提是当前线程占有锁(在synchronized中)
-
两者都可以被interrupted方法中断
1.3 并发和并行
-
串行
一次只能取得一个任务,并执行这个任务
-
并行
多项工作一起执行,之后汇总
eg: 泡方便面
-
并发
同一时刻多个线程在访问同一个资源,多个线程对一个点
1.4 管程(monitor)
管程是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只能被一个进程调用.但是这样并不能保证进程以设计的顺序执行
执行线程首先要持有管程对象, 然后才能执行方法, 当方法完成之后会释放管程, 方法在执行时候会持有管程, 其他线程无法在获取同一个管程
1.5 用户线程和守护线程
用户线程: 平时用到的普通线程,自定义线程
守护线程: 运行在后台,是一种特殊的线程,eg: 垃圾回收
注意:守护线程结束, 主线程不一定结束; 主线程结束, 守护线程一定结束
Thread t1 = new Thread(()->{
System.out.println(Thread.currentThread().getName()+"::"+Thread.currentThread().isDaemon());
},"t1");
//设置守护线程
t1.setDaemon(true);
二 lock接口
2.1 synchronized
2.1.1 修饰对象
-
修饰一个同步代码块, 作用的对象是调用这个代码块的对象.
-
修饰一个方法, 该方法称为同步方法
-
修饰一个静态的方法
-
修饰一个类
案例 (模拟买票):
public class Test02 {
public static void main(String[] args) {
Ticket ticket = new Ticket();
new Thread(()->{
while (Ticket.number>0){
ticket.sell();
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
}
},"t1").start();
new Thread(()->{
while (Ticket.number>0){
ticket.sell();
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
}
},"t2").start();
new Thread(()->{
while (Ticket.number>0){
ticket.sell();
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
}
},"t3").start();
}
}
class Ticket{
static public int number = 30;
public synchronized void sell(){
if (number>0){
System.out.println(Thread.currentThread().getName()+"正在卖"+(number--)+"票");
}
}
}
2.2 Lock
Lock锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作
与synchronized的区别
Lock | synchronized |
---|---|
不是内置的 | java内置 |
一个类 | 关键字 |
需要用户手动释放锁(不释放的话会出现死锁现象) | 不需要用户手动释放锁 |
lock.lock //加锁
lock.unlock //释放锁
-
newCondition
返回一个Condition对象, Condition类也可以实现等待/通知模式
-
notify()通知, jvm会随机唤醒某个等待的线程, 使用Condition类可以进行选择性通知, Condition常用的两个方法
-
await() 使当前线程等待
-
signal() 唤醒一个等待的线程
-
-
2.3 ReentrantLock
ReentrantLock, 可重入锁, 是唯一实现了Lock接口的类.
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(()->{
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" come out");
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName()+" come in");
}finally {
reentrantLock.unlock();
}
}finally {
reentrantLock.unlock();
}
},"t1").start();
2.4 ReadWriteLock
ReadWriteLock是一个接口, 主要有两个方法
Lock readLock();
Lock writeLock();
一个用来获取读锁, 一个用来获取写锁.
将文件的读写操作分开, 分成2个锁来分配给线程, 从而使得多个线程可以同时进行读操作
ReentrantReadWriteLock实现了ReadWriteLock接口
class My{
Map<String,String> map = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(String key,String value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t正在写入");
map.put(key,value);
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}finally {
lock.writeLock().unlock();
}
}
public void read(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t正在读取");
try {
TimeUnit.MILLISECONDS.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t读取完成"+"\t"+map.get(key));
}finally {
lock.readLock().unlock();
}
}
}
public class Test1 {
public static void main(String[] args) {
My my = new My();
for (int i = 0; i < 10; i++) {
int FIN = i;
new Thread(()->{
my.write(FIN+"",FIN+"");
},String.valueOf(i)).start();
}
for (int i = 0; i < 10; i++) {
int FIN = i;
new Thread(()->{
my.read(FIN+"");
},String.valueOf(i)).start();
}
}
}
-
如果一个线程占用了读锁, 此时其他线程如果要申请写锁, 则申请写锁的线程会一直等待释放读锁
-
如果一个线程占用了写锁, 此时其他线程如果要申请写锁或读锁, 则申请写锁的线程会一直等待释放写锁
2.5 小结
-
Lock是一个接口,synchronized是一个关键字
-
synchronized在发生异常时, 会自动释放线程占有的锁, 因此不会导致死锁发生; 而lock在发生异常时, 如果没有主动通过unLock()去释放锁, 则很可能造成死锁现象, 因此需要在finally块中释放锁
-
lock可以让等待的线程响应中断, 而synchronized却不行, 使用synchronized时, 等待的线程会一直等待下去, 不能响应中断
-
通过lock可以知道有没有成功获取锁,而synchronized则不行
-
lock可以提高多个线程进行读操作的效率
-
如果资源竞争不激烈, 两者的性能差不多, 如果竞争激烈的话, lock的性能要远远优于synchronized
三 线程间通信
-
共享内存
-
消息传递
volatile 关键字实现线程交替加减
eg: 两个线程,一个线程对当前数值加 1,另一个线程对当前数值减 1,要求 用线程间通信
3.1 用synchronized
class My{
private int number = 0;
public synchronized void insert(){
try {
while (number!=0){
this.wait();
}
number++;
System.out.println(Thread.currentThread().getName()+"加成功\t值为"+number);
notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
public synchronized void dec(){
try {
while (number==0){
this.wait();
}
number--;
System.out.println(Thread.currentThread().getName()+"减成功\t值为"+number);
notifyAll();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Test1 {
public static void main(String[] args) {
My my = new My();
new Thread(()->{
for (int i = 0; i < 5; i++) {
my.insert();
}
},"t1").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
my.dec();
}
},"t2").start();
}
}
3.2 用Lock
class My{
private int number = 0;
//锁
private Lock lock = new ReentrantLock();
//钥匙
private Condition condition = lock.newCondition();
public void insert(){
try {
lock.lock();
while (number!=0){
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName()+"加成功\t值为"+number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void drc(){
try {
lock.lock();
while (number==0){
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+"减成功\t值为"+number);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class Test1 {
public static void main(String[] args) {
My my = new My();
new Thread(()->{
for (int i = 0; i < 5; i++) {
my.insert();
}
},"t1").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
my.drc();
}
},"t2").start();
}
}
3.3 线程间定制化通信
eg: A 线程打印 5 次 A,B 线程打印 10 次 B,C 线程打印 15 次 C,按照 此顺序循环 10 轮
class My{
private int number = 0;
//锁
private Lock lock = new ReentrantLock();
//钥匙
private Condition conditionA = lock.newCondition();
private Condition conditionB = lock.newCondition();
private Condition conditionC = lock.newCondition();
public void insertA(int i){
try {
lock.lock();
while (number!=0){
conditionA.await();
}
for (int i1 = 0; i1 < 5; i1++) {
System.out.println(Thread.currentThread().getName()+"输出A\t第"+i+"轮");
}
number=1;
conditionB.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void insertB(int i){
try {
lock.lock();
while (number!=1){
conditionB.await();
}
for (int i1 = 0; i1 < 10; i1++) {
System.out.println(Thread.currentThread().getName()+"输出B\t第"+i+"轮");
}
number=2;
conditionC.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void insertC(int i){
try {
lock.lock();
while (number!=2){
conditionC.await();
}
for (int i1 = 0; i1 < 15; i1++) {
System.out.println(Thread.currentThread().getName()+"输出C\t第"+i+"轮");
}
number=0;
conditionA.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
public class Test1 {
public static void main(String[] args) {
My my = new My();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
my.insertA(i);
}
},"t1").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
my.insertB(i);
}
},"t2").start();
new Thread(()->{
for (int i = 1; i <= 10; i++) {
my.insertC(i);
}
},"t3").start();
}
}
synchronized实现同步的基础: java中的每一个对象都可以作为锁, 具体变现为3种形式
-
普通同步方法, 锁的是当前实例对象
-
静态同步方法, 锁的是当前类的Class对象
-
同步方法块, 锁的是Synchronized括号里配置的对象
四 集合的线程安全
4.1 Vector(线程安全的)
Vector是矢量队列,继承于AbstractList, 实现了List, RandomAccess, Cloneable 接口. 支持队列所支持的添加, 删除, 修改 等功能, 还有随机访问的功能.
在Vector中, 我们可以通过元素的序号快速获取元素对象; 这就是快速随机访问. Vector实现了Cloneable接口, 实现clone()函数 , 它能被克隆
List<String> list = new Vector<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
},"线程"+i).start();
}
注:Vector的add方法加锁了
4.2 Collections
Collections 提供了方法 synchronizedList 保证 list 是同步线程安全的
List<String> list =Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString());
System.out.println(list);
},"线程"+i).start();
}
4.3* CopyOnWriteArrayList
相当于线程安全的ArrayList, 可变数组
-
独占锁效率低: 采用读写分离思想解决
-
写线程获得到锁, 其他写线程阻塞
-
复制思想:
-
先复制出一个新的容器, 然后将新元素添加到新容器中, 然后将引用指向新的容器如果遇到写线程还没来得及写回内存, 其他线程就会读到脏数据这就是CopyOnWriteArrayList的思想和原理: 就是先拷贝一份
-
五 多线程锁
锁的执行顺序: 不同步锁>同步锁>静态同步锁
5.1 公平锁和非公平锁
-
公平锁: 效率低, cpu利用率低
-
非公平锁: 效率高, 线程容易饿死(所有工作, 一个线程做完了)
ReentrantLock 默认非公平锁, 想要变成公平锁构造方法里加ture
private ReentrantLock reentrantLock =new ReentrantLock(true);
部分底层源码
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
5.2 可重入锁
synchronized和lock都是可重入锁
-
synchronized是隐式锁, 不用手动上锁和解锁, 而lock是显示锁, 需要手动加锁和解锁
-
可重入锁也叫递归锁
-
可重入锁可以级联进入, 不需要在重新获取锁
synchronized锁
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
synchronized (o){
System.out.println("外锁");
synchronized (o){
System.out.println("内锁");
}
}
}).start();
}
ReentrantLock锁
public static void main(String[] args) {
ReentrantLock reentrantLock = new ReentrantLock();
new Thread(()->{
reentrantLock.lock();
try {
System.out.println("外锁");
reentrantLock.lock();
try {
System.out.println("内锁");
}finally {
reentrantLock.unlock();
}
}finally {
reentrantLock.unlock();
}
}).start();
}
5.3 死锁
两个或两个以上的进程因抢夺同一个资源而造成相互等待的现象叫死锁
-
造成死锁的原因
-
系统资源不足
-
系统资源分配不当
-
程序运行顺序不当
-
-
查看是否死锁
-
jps 查看进程号
-
jstack id 自带的堆栈跟踪工具
-
死锁案例
public static void main(String[] args) {
Object a = new Object();
Object b = new Object();
new Thread(()->{
synchronized (a){
System.out.println(Thread.currentThread().getName()+"持有a, 尝试获取b");
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
synchronized (b){
System.out.println("获取到b");
}
}
},"a").start();
new Thread(()->{
synchronized (b){
System.out.println(Thread.currentThread().getName()+"持有b, 尝试获取a");
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
synchronized (a){
System.out.println("获取到a");
}
}
},"b").start();
}
六 Callable&Future 接口
6.1 Callable接口
Callable接口解决了Runnable接口的缺陷: 线程可以返回结果
-
为了实现Runnable, 需要实现run()方法, 对于Callable, 需要实现call()方法
-
call()方法可以引发异常, 而run()不能
-
必须实现call方法
6.2 Futher接口
用futher接口来接收主线程返回的对象
重要方法:
-
public boolean cancel(boolean mayInterrupt) 用于停止任务
如果未启动 , 它会停止任务 , 当mayInterrupt为true时才会中断任务
-
public Object get() 用于获取任务的结果
如果任务完成,它将立即返回结果, 否则等任务完成,返回结果
-
public boolean isDone(): 如果任务完成 , 则返回true , 否则返回false
public static void main(String[] args) throws ExecutionException, InterruptedException, TimeoutException {
FutureTask<Integer> futureTask = new FutureTask<>(() -> 1111);
Thread thread = new Thread(futureTask,"t1");
thread.start();
try {
TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"::"+futureTask.get(1,TimeUnit.SECONDS));
}
6.3* 小结
-
在主线程中需要执行比较耗时的操作时, 但又不想阻塞主线程 , 可以把这些任务交给Future对象在后台执行 , 当主线程需要时, 就可以通过Future对象获得后台作业的计算结果
-
只有在计算完成时才能检索结果; 如果计算尚未完成 , 则阻塞get方法 , 一旦完成, 就不能重新开始.
-
只计算一次
七 JUC三大辅助类
-
CountDownLatch 减少计数
-
CyclicBarrier: 循环栅栏
-
Semaphore: 信号灯
7.1 CountDownLatch
CountDownLatch类可以设置一个计数器, 然后通过countDown方法进行减一的操作 , 使用await方法等待计数器不大于0 , 然后继续执行await方法之后的语句
常用方法:
-
其他线程调用countDown方法会将计数器减一(调用countDown方法的线程不会阻塞)
-
当计数器的值变为0时, 因await方法阻塞的线程会被唤醒 , 继续执行
eg: 6个同学陆续离开教室后关门
public static void main(String[] args) {
CountDownLatch count = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"离开教师");
count.countDown();
},String.valueOf(i)).start();
}
try {
count.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"关门");
}
7.2 CyclicBarrier
CyclicBarrier的构造方法第一个参数是目标障碍数, 每次执行CyclicBarrier一次 , 障碍数就会加一 , 如果达到了目标障碍数 , 才会执行cyclicBarrier.await()之后的语句 . 可以将CyclicBarrier理解为加一操作
eg:召唤神龙
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7, new Runnable() {
@Override
public void run() {
System.out.println("出现吧!神龙");
}
});
for (int i = 0; i < 7; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"星珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
},String.valueOf(i+1)).start();
}
}
7.3 Semaphore
Semaphore的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池), 每个信号量初始化为一个最多只能分发一个许可证. 使用acquire方法获得许可证 , release方法释放许可
eg:抢车位 , 三个车位 , 六辆汽车
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到了车位");
try {
TimeUnit.SECONDS.sleep(new Random().nextInt(5));} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"--------离开了");
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
},String.valueOf(i+1)).start();
}
}
八 读写锁
8.1 前置知识
-
悲观锁: 在每次操作的时候, 都会先加锁 , 使用时解锁
-
乐观锁: 干啥都不加锁
住: 在解决多线程安全问题的时候 , 通过比较版本号来同步数据
表锁|行锁|读锁|写锁
-
表锁: 操作整个表 , 不会发生死锁
-
行锁: 每个表中的单独一行加锁 , 会死锁
-
读锁: 共享锁, 会死锁
-
写锁: 独占锁, 会死锁
8.2 读写锁概述
Java的并发包提供了读写锁ReentrantReadWriteLock, 它表示两个锁 , 一个是读操作相关的锁, 称为共享锁 ; 一个是写操作相关的锁 , 称为排他锁
-
线程进入读锁的前提条件:
-
没有其他线程的写锁
-
没有且请求, 或者有写请求,但调用线程和持有锁的线程是同一个(可重入锁)
-
-
线程进入写锁的前提条件:
-
没有其他线程的读锁
-
没有其他线程的写锁
-
8.3 读写锁的三个重要特性
-
公平选择性: 支持非公平(默认)和公平的锁获取方式 , 吞吐量还是非公平优于公平
-
重进入: 读锁和写锁都支持线程重进入
-
锁降级: 遵循获取写锁, 获取读锁再释放写锁的次序 , 写锁能够降级为读锁
8.4 ReentrantReadWriteLock
-
读锁ReentrantReadWriteLock.ReadLock , readLock()方法
-
写锁ReentrantReadWriteLock.WriteLock, writeLock()方法
class MyResource{
Map<String,String> map = new HashMap<>();
ReadWriteLock lock = new ReentrantReadWriteLock();
public void write(String key,String value){
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t正在写入");
map.put(key, value);
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t写入完成");
}finally {
lock.writeLock().unlock();
}
}
public void read(String key){
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t正在读取");
String s = map.get(key);
try {
TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"\t完成读取\t"+s);
}finally {
lock.readLock().unlock();
}
}
}
public class Demo4 {
public static void main(String[] args) {
MyResource myResource = new MyResource();
for (int i = 0; i < 10; i++) {
int FinalI = i+1;
new Thread(()->{
myResource.write(FinalI+"",FinalI+"");
},String.valueOf(i+1)).start();
}
for (int i = 0; i < 10; i++) {
int FinalI = i+1;
new Thread(()->{
myResource.read(FinalI+"");
},String.valueOf(i+1)).start();
}
}
}
8.5 读写锁的演变
无锁: 多线程抢夺资源
synchronized和ReentrantLock 都是独占锁 , 每次只可以一个操作 , 不能共享
ReentrantReadWriteLock 读读可以共享, 提升性能, 但是不能多人写 ,
缺点: 造成死锁(一直读 , 不能写), 读进程不能写 , 写进程可以读
写锁可以降级为读锁(写锁等级)
锁降级的步骤
获取写锁->获取读锁->释放写锁->释放读锁
public static void main(String[] args) {
ReentrantReadWriteLock reentrantLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantLock.writeLock();
writeLock.lock();
readLock.lock();
writeLock.unlock();
readLock.unlock();
}
8.6 锁降级的必要性
读写锁遵循的规则
如果有一个线程已经占用了读锁 , 此时有其他线程如果要申请读锁 , 可以申请成功
如果一个线程已经占用了读锁,则此时其他线程如果要申请写锁 , 则申请写锁的线程会一直等待释放读锁 , 因为读写不能同时操作
如果有一个线程已经占用了写锁 , 则此时其他线程如果申请写锁或者读锁 , 都必须等待之前的线程释放写锁
在完成部分写操作后 , 退而使用读锁降级 , 来允许响应其他进程的读操作 , 只有当全部事务完成后才真正释放锁
九 阻塞队列
9.1 概述
阻塞队列是共享队列(多线程操作) , 一端输入 , 一段输出 , 不能无限放队列 , 满了之后就会进入阻塞 , 队列为空也阻塞
当队列是空时, 从队列中获取元素的操作将会被阻塞
当队列是满的 , 从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞 , 直到其他线程往空的队列中插入新的元素
试图向满的队列中获取元素的线程会被阻塞 , 直到其他线程重队列中移除一个或多个元素或全部清空 , 使得队列变得空闲并后续新增
常见的队列:
-
先进先出(FIFO): 先插入的队列元素最先出队列
-
后进先出(LIFO):后插入队列的元素最先出队列
9.2 核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
抛出异常 | 当阻塞队列满时, 再往队列里add插入元素会抛IllegalStateException:Queue full 当阻塞队列空时, 再往队列里remove移除元素会抛NoSuchElementException |
特殊值 | 插入方法 , 成功true 失败false 移除方法 , 成功返回出队列的元素 , 队列里没有就返回null |
一直阻塞 | 当阻塞队列满时, 生产者线程继续往队列里put元素 , 队列会一直阻塞生产者线程知道put数据or响应中断退出 当阻塞队列空时, 消费者线程试图从队列中take元素 , 队列会一直阻塞消费者线程知道队列可用 |
超时退出 | 当阻塞队列满时 , 队列会阻塞生产者线程一定时间 , 超过限时后生产者线程会退出 |
-
放入数据
-
offer(a) : 表示如果可能的话 , 将a加到BlockingQueue里, 如果队列未满 , 则返回true , 否则false(不会阻塞当前执行方法的线程)
-
offer(E o,long timeout,TimeUnit unit):如果在规定时间内未加入队列 ,返回false
-
put(a): 和offer方法一样 , 但是如果没放入队列的话,会阻塞知道放进去再继续
-
-
获取数据
-
poll(time):取走队列的首个元素 , 如果在time时间内没有取出的话返回null
-
poll(long timeout, TimeUnit unit):从队列取出一个队首的对象, 指定时间内如果有数据可取,则立即返回队列中的数据。否则返回失败。
-
take(): 取走 队列里排在首位的对象,若 队列为空,阻断 进入等待状态直到 BlockingQueue 有新的数据被加入
-
drainTo(): 一次性从 队列获取所有可用的数据对象(还可以指定 获取数据的个数),通过该方法,可以提升获取数据效率;不需要多次分批加 锁或释放锁。
-
9.3 分类
-
ArrayBlockingQueue(常用)
-
基于数组的阻塞队列
-
由数组结构组成的有界阻塞队列
-
ArrayBlockingQueue在生产者放入数据和消费者获取数据, 都是共用同一个锁对象 , 无法并行
-
-
LinkedBlockingQueue(常用)
-
基于链表的阻塞队列
-
由链表结构组成的有界(但大小默认值为我Integer.MAX_VALUE)阻塞队列
-
之所以能够高效的处理并发数据 , 还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步 , 这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据 , 以此来提高整个队列的并发性能
-
-
DelayQueue
-
DelayQueue中的元素只有当其指定的延时时间到了 , 才能从队列中获取该元素
-
使用优先队列实现的延时无界阻塞队列
-
DelayQueue是一个无界阻塞队列 , 生产者永远不会被阻塞 , 而消费者会被阻塞
-
-
PriorityBlockingQueue
-
基于优先级的阻塞队列
-
支持优先级排序的无界阻塞队列
-
不会阻塞生产者 , 在没有数据可用的时候, 阻塞消费者 (生产者生产数据的速度绝对不能快于消费者消费数据的速度 , 否则时间一长 , 会最终耗尽所有的可用堆内存空间)
-
锁同步机制采用的是公平锁
-
-
SynchronousQueue
-
无缓冲的等待队列 , 少了一个中间经销商的缓解(缓冲区)
-
不存储元素的阻塞队列 , 即单个元素的队列
-
两种声明方式
-
公平模式: 采用公平锁 , 配合一个FIFO队列在阻塞
-
非公平模式(默认): 采用非公平锁 , 同时配合LIFO队列来管理多余的生产者和消费者
非公平模式 , 如果生产者和消费者的处理速度有差距 , 则很容易出现饥饿的情况 , 有可能某些生产者或者消费者的数据永远得不到处理
-
-
-
LinkedTransferQueue
-
相对于其他的阻塞队列 , 多了tryTransfer和transfer方法
-
由链表组成的无界阻塞队列
-
采用预占模式
预占模式: 消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素 为 null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时 发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到 该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的 方法返回。
-
-
LinkedBlockingDeque
-
由链表组成的双向阻塞队列
-
可以从队 列的两端插入和移除元素
-
阻塞情况
-
插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时 再讲该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作 失败,也可以不设置超时参数一直阻塞,中断后抛出 InterruptedException 异常
-
读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可 以通过设置超时参数
-
-
十 ThreadPool 线程池
10.1 概述
连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用
线程池一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分
配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程
数量超过了最大数量,超过数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
特点:
-
降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的消耗
-
提高响应速度: 当任务到达时 , 任务可以不需要等待线程创建就能立即执行
-
提高线程的可管理性: 线程是稀缺资源 , 如果无限制的创建 , 不仅会消耗系统资源 , 还会减低系统的稳定性, 使用了那个线程池可以进行统一的分配, 调优和监控
架构
10.2 线程池种类(只列举常用)
-
一池一线程
ExecutorService ThreadPool1 = Executors.newSingleThreadExecutor()
场景: 适用于需要保证顺序执行各个任务,并且在任意时间点,不会同时有多个 线程的场景
eg:
ExecutorService threadPool1 = Executors.newSingleThreadExecutor(); try { for (int i = 1; i <= 10; i++) { threadPool1.execute(() -> System.out.println(Thread.currentThread().getName()+"在执行公务")); } }finally { threadPool1.shutdown(); }
-
一池多线程
场景: 适用于可以预测线程数量的业务中,或者服务器负载较重,对线程数有严 格限制的场景
ExecutorService threadPool = Executors.newFixedThreadPool(6);
eg:
ExecutorService threadPool2 = Executors.newFixedThreadPool(6); try { for (int i = 1; i <= 10; i++) { threadPool2.execute(() -> System.out.println(Thread.currentThread().getName()+"在执行公务")); } }finally { threadPool2.shutdown(); }
-
一池可扩容线程
场景: 适用于创建一个可无限扩大的线程池,服务器负载压力较轻,执行时间较 短,任务多的场景
ExecutorService threadPool = Executors.newCachedThreadPool();
eg
ExecutorService threadPool3 = Executors.newCachedThreadPool();
try {
for (int i = 1; i <= 10; i++) {
threadPool3.execute(() -> System.out.println(Thread.currentThread().getName()+"在执行公务"));
}
}finally {
threadPool3.shutdown();
}
执行线程: execute()
关闭线程:shutdown()
10.3 线程池参数
int corePoolSize 线程池的核心线程数
int maximumPoolSize 最大线程数
long keepAliveTime 空闲线程存活时间
TimeUnit unit存活时间单位
BlockingQueue workQueue阻塞队列
ThreadFactory threadFactory 线程工厂
RejectedExecutionHandler handler 拒绝策略
当提交任务数大于 corePoolSize 的时候,会优先将任务放到 workQueue 阻塞队列中。当阻塞队列饱和后,会扩充线程池中线程数,直
到达到 maximumPoolSize 最大线程数配置。此时,再多余的任务,则会触发线程池 的拒绝策略了。
总结:当提交的任务数大于(workQueue.size() + maximumPoolSize ),就会触发线程池的拒绝策略。
10.4 拒绝策略
-
CallerRunsPolicy: 当触发拒绝策略 , 只要线程池没关闭的话 , 会调用线程直接运行任务 , 一般并发比较小 , 不允许失败 .
-
AbortPolicy(默认的拒绝策略): 丢弃任务 , 并抛出拒绝执行RejectedExecution异常.
-
DiscardPolicy: 直接丢弃
-
DiscardOldestPolicy: 当触发拒绝策略 , 只要线程池没有关闭的话 , 丢弃阻塞队列workQueue中最老的一个任务 , 将新任务加入
10.5 自定义线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3,3,10L, TimeUnit.SECONDS,new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
try {
for (int i = 1; i <= 10; i++) {
poolExecutor.execute(()->{
System.out.println(Thread.currentThread().getName()+"窗口正在买票");
try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"窗口买票结束");
});
}
}finally {
poolExecutor.shutdown();
}
十一 Fork/Join
将一个大的任务拆分成多个子任务进行合并处理 , 最后将子任务结果合并成最后的计算结果
Fork: 把一个复杂任务进行分拆 , 大事化小
Join: 把分拆任务的结果进行合并
ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况
下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
RecursiveAction:用于没有返回结果的任务
RecursiveTask:用于有返回结果的任务
ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
RecursiveTask:继承后可以实现递归(自己调自己)调用的任务
创建分支合并对象 通过该对象调用内部方法
eg:1加到100,相加两个数值不能大于10
class MyTask extends RecursiveTask<Integer>{
private static final Integer VALUE = 10;
private int begin;
private int end;
private int result;
public MyTask(int begin, int end) {
this.begin = begin;
this.end = end;
}
@Override
protected Integer compute() {
if ((end-begin)<=10){
for (int i = begin; i <= end; i++) {
result = result+i;
}
}else {
int middle = (begin+end)/2;
MyTask task = new MyTask(begin, middle);
MyTask task1 = new MyTask(middle + 1, end);
task.fork();
task1.fork();
result = task.join()+task1.join();
}
return result;
}
}
public class Demo3 {
public static void main(String[] args) {
MyTask myTask = new MyTask(0, 100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);
Integer integer = null;
try {
integer = submit.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(integer);
}
}
十二 CompletableFuture
12.1 概述
CompletableFuture实现了Future接口 , 用于异步编程 , 异步通常意味着非阻塞 , 可以使得我们的任务单独运行在与主线程分离的其他
线释放锁程中,并且通过回调可 以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
Future 的主要缺点如下:
-
不支持手动完成
-
不支持进一步的非阻塞调用
-
不支持链式调用
-
不支持多个 Future 合并
-
不支持异常处理
12.2 runAsync
runAsync是无返回值的异步
public static void main(String[] args) {
ExecutorService executorService =null;
try {
executorService = Executors.newFixedThreadPool(3);
CompletableFuture<Void>completableFuture = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
},executorService);
try {
System.out.println(completableFuture.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}finally {
executorService.shutdown();
}
}
注意: 主线程调用 get 方法会阻塞
12.3 supplyAsync
supplyAsync是带返回值的异步
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor =null;
try{
poolExecutor = new ThreadPoolExecutor(3,5,10L, TimeUnit.MINUTES,new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("一秒后"+result);
return result;
},poolExecutor);
try {
System.out.println(completableFuture.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}finally{
poolExecutor.shutdown();
}
}
12.4 supplyAsync的链式编程
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor =null;
try {
poolExecutor = new ThreadPoolExecutor(3,5,10L, TimeUnit.MINUTES,new LinkedBlockingDeque<>(), Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy());
CompletableFuture<Integer> completableFuture = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"come in");
int result = ThreadLocalRandom.current().nextInt(10);
try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
System.out.println("一秒后"+result);
return result;
},poolExecutor).thenApply(new Function<Integer, Integer>() {
@Override
public Integer apply(Integer integer) {
return integer+10;
}
}).whenComplete(new BiConsumer<Integer, Throwable>() {
@Override
public void accept(Integer integer, Throwable throwable) {
if (throwable==null){
System.out.println("值为: "+integer);
}
}
}).exceptionally(new Function<Throwable, Integer>() {
@Override
public Integer apply(Throwable throwable) {
throwable.printStackTrace();
System.out.println("异常情况"+throwable.getCause()+"\t"+throwable.getMessage());
return null;
}
});
System.out.println(Thread.currentThread().getName() + "忙其他任务");
try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {e.printStackTrace();}
} catch (Exception e) {
e.printStackTrace();
}finally {
poolExecutor.shutdown();
}
}
注解:
-
thenApply()
返回一个新的 CompletionStage,当此阶段正常完成时,将执行该阶段的结果作为所提供函数的参数。 此方法类似于 Optional.map 和 Stream.map。
-
whenComplete()
返回与此阶段具有相同结果或异常的新 CompletionStage,该阶段在此阶段完成时执行给定的操作。 此阶段完成后,将调用给定的操作,并将此阶段的结果(或如果没有)和异常(或nullnull如果没有)作为参数。当操作返回时,返回的阶段将完成。 与 方法 handle不同,此方法不是为了转换完成结果而设计的,因此提供的操作不应引发异常。但是,如果是这样,则以下规则适用:如果此阶段正常完成,但提供的操作引发异常,则返回的阶段异常完成,并提供所提供操作的异常。或者,如果此阶段异常完成,并且提供的操作引发异常,则返回的阶段异常完成,并显示此阶段的异常。
-
exceptionally()
返回一个新的 CompletionStage,当此阶段异常完成时,将使用此阶段的异常作为所提供函数的参数执行。否则,如果此阶段正常完成,则返回的阶段也以相同的值正常完成.