JUC
JUC概述
指的是java.util.concurrent包,处理并发编程的工具包,jdk1.5开始
进程:系统进行资源分配的基本单位,一个正在执行的程序
线程:操作系统能够进行运算调度的基本单位,包含在进程中
线程的状态:Thread.State枚举类
- NEW(新建)
- RUNNABLE(准备就绪)
- BLOCKED(阻塞)
- WAITING(等待)
- TIMED_WAITING(超时后不再等待)
- TERMINATED(终结)
wait和sleep:
- wait是Object中的方法,任何对象实例都可以调用,表示释放锁,调用前提是当前线程占有锁(synchronized)
- sleep是Thread的静态方法,调用时不会释放锁
- 两者都可以被interrupted方法中断
并发与并行
管程:
- jvm的同步是基于进入和退出管程对象实现的,每个对象都有一个管程(monitor)对象,会随着对象一同创建和销毁,其实就是锁的概念。
- 线程要执行方法首先要持有管程对象,然后才能执行方法,当方法执行完成后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程。
用户线程和守护线程:
- 用户线程:自定义线程
- 守护线程:是一种特殊的线程,是为其他线程服务的,如垃圾回收线程,当jvm中没有用户线程时,jvm退出,作为服务线程,没有要服务的对象,自然就没有必要运行了
Lock接口
synchronized关键字
多线程编程步骤:
- 创建资源类(有属性,有方法),操作方法上进行判断、干活、通知
- 创建多个线程,调用资源类的操作方法
- 防止虚假唤醒(wait的位置需要放在最后或者放在while条件中)
ReentrantLock:可重入锁(我的理解是持有当前锁使可以再持有当前锁,比如递归调用)
- Lock锁实现提供了比使用同步方法和语句更广泛的锁操作,允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象,lock提供了比synchronized更多的功能
- synchronized不需要用户释放锁,会自动释放,Lock需要手动释放
- synchronized不响应中断,Lock可以响应中断
- Lock可以知道有没有获取到锁
- Lock可以提高多个线程进行读操作的效率,如果锁竞争激烈,Lock的性能远大于synchronized
- new Thread().start()并不一定会马上创建线程,因为start之后就交给操作系统了,操作系统可能马上创建,也可能稍后创建
线程间通信
使用同一把锁Lock lock = new ReentrantLock()
,调用lock.newCondition()
获取多个Condition,Condition之间可以相互唤醒
wait()/notify()可以实现等待/通知,Lock的newCondition也可以实现,notify会随机唤醒一个线程,Condition可以选择性通知(定制化通信)。
await()会使当前线程a等待,并释放锁,当其他线程调用了a.signal()时,会从当前的Condition对象的等待队列中唤醒一个线程,唤醒的线程会尝试获得锁,一旦获取成功就会继续执行,如果只有一个线程使用了当前Condition,且该线程没有在await,其实唤不唤醒都一样,因为本身就在执行,如果它在await()那么它会被唤醒。
集合的线程安全
线程安全的集合工具类,将线程不安全的集合或map变成线程安全的:
- Collections.synchronizedList()
- Collections.synchronizedSet()
- Collections.synchronizedMap()
线程安全的List:Vector,CopyOnWriteArrayList(写时复制,适用场景:签到)
线程安全的Set:CopyOnWriteArraySet(内部使用了CopyOnWriteArrayList)
线程安全的Map:ConcurrentHashMap
锁的种类
- 公平锁、非公平锁:多个线程是否抢占资源
- 可重入锁:递归锁,持有当前锁后,可以再进入需要当前锁才可进入的资源
- 死锁:多个线程因为争夺资源而互相等待的现象
排查死锁方法:
- jps -l查看jvm进程,-l可以列出全类名
- jstack PID,命令会分析是否有死锁
Callable接口
Thread只接收Runnable接口的参数,线程没有返回值,对于需要线程执行结果的情况,需要使用Callable接口。
通过创建FutureTask将Callable接口参数转换为一个Runnable传递给Thread进行执行该Task(FutureTask实现了Runnable接口,又包含了Callable属性,Thread调run(),run()调call(),并记录结果在FutureTask的属性上),而主线程可以继续执行其逻辑,在需要FutureTask执行结果的时候调用其get方法。
JUC实用类
- 减少计数:CountDownLatch,在完成一组正在其他线程中执行的操作之前,允许一个或多个线程等待
- 用给定的计数初始化,在countDown调用使计数达到0之前,await方法会一直阻塞。达到0时会释放所有等待的线程,并且所有再调用await的方法都将立即返回。
- 这种现象只能用一次,也就是计数无法被重置,如果需要重置,考虑使用CyclicBarrier
- 用于指定计数之后,某些线程可以继续开始执行(可以是自己)
当countDown()被任意线程调用指定次数后(构造器指定的),所有调用await方法的线程被唤醒。(比如击鼓传花,10个线程,任意5个线程完成了countDown,另外一个或多个线程就要做某些事)
latch是门闩、插销、弹簧锁的意思,所以CountDownLatch有倒计时就触发的意思
例如:教室里有5个同学,当5个同学都离开之后(调用countDown),班长锁门(countDown调用完之前是await的状态,调用完之后会被唤醒)
- 循环栅栏:CyclicBarrier
- 初始化参数为屏障点n和Runnable,当有n个线程调用了await时,就会触发Runnable,并唤醒总共await的这n个线程。
- 用于指定数量线程await之后,这些线程可以继续同时开始执行(相互等待对方,在触发之前也可以执行一次Runnable),之后再有指定数量线程await之后,还会相互等待,循环往复
当一组线程需要相互等待时,CyclicBarrier很有用
例子:召集7颗龙珠即可召唤神龙,如果有7个线程都调用了await方法,那么就会触发Runnable,再有7个调用await方法,会再次触发调用Runnable
- 信号灯:Semaphore
- 可以设置指定数量的许可,供其他线程acquire,使用后release许可,供其他线程继续使用
- 用于指定数量的线程执行,执行完之后,释放响应资源,其他线程才能获取到资源然后执行
例子:6辆车3个车位,相当于6个线程抢占3个车位,每个线程acquire之后,停留一段时间release,其他线程可以继续获取到车位
读写锁
读锁:共享锁,一个锁可能同时被多个线程获取,多个读锁之间可能发生死锁(多个共享资源都加了读锁,互相读的过程中又要互相写)
写锁:独占锁,一个锁只能被一个线程获取,多个写锁直接可能发生死锁(多个共享资源都加了写锁,互相写导致互相等待)
- ReentrantReadWriteLock读写锁,一个资源可以被多个读线程访问,或者被一个写线程访问,但是不能存在读写线程,读写互斥,读读共享
- 获取写锁时,需要等待所有线程的读锁释放(包括自己的读锁,所以不能获取读锁后又获取写锁,会死锁),即需要等待前面的读完才能写或者说前面没有人读了才能写
- 获取读锁时,需要等待写锁线程释放锁(如果是自己的写锁,不需要释放),如果有线程持有写锁或正在获取写锁,要等待该线程释放写锁或获取写锁并释放写锁后才能获取读锁,即需要等待前面的写完或想要写的写完才能读或者说前面没有人想写了才能读
锁的演变:
- 独占锁:读写都是单线程操作(synchronized、ReentrantLock),缺点是读读不能共享
- 读写锁(ReentrainReadWriteLock),读读共享,缺点:锁饥饿,一直读或一直写,其他线程就没法写或读了;读的时候不能写,只能读完后再写,写的时候可以读
锁降级:
- 获取写锁 -> 获取读锁 -> 释放写锁 -> 释放读锁,从写锁降级为读锁了,提高数据的可见性
- 不能从读锁升级为写锁
阻塞队列
concurrent包中,BlockingQueue很好的解决了多线程中,如何高效传输数据的问题,通过这些高效并且线程安全的队列类,为我们快速搭建高质量的多线程程序带来极大的便利。
BlockingQueue的核心方法:
- 会抛出异常的方法:add(e),remove(),element(),队列满添加元素会抛异常,队列空移除元素会抛异常,队列空检查元素会抛异常
- 不会抛出异常的方法:offer(e),poll(e),peek(),分别表示添加元素、移除元素、检查元素
- 会阻塞的方法:put(e),take(),分别表示添加元素、移除元素,队列满添加元素会阻塞,队列空移除元素会阻塞,或者被中断
- offer和poll可以配置超时参数
常见的BlockingQueue
- ArrayBlockingQueue,基于数组的有界队列,使用一把锁
- LinkedBlockingQueue,基于链表的队列,默认最大为Integer.MAX_VALUE,两把锁
- DelayQueue,使用优先级队列实现的延迟无界阻塞队列
- 元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue是一个没有大小限制的队列,因此往队列中插入数据的阻塞作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞
- PriorityBlockingQueue,基于优先级的无界阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),内部控制线程同步的锁采用的是公平锁
- 不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者,因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间
- SynchronousQueue,一种无缓冲的等待队列,不存储元素的阻塞队列,也即单个元素的队列
- 声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为
- 公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略
- 非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理
- LinkedTransferQueue,由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,LinkedTransferQueue多了tryTransfer和transfer方法
- LinkedTransferQueue采用一种预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程被等待在这个节点上,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回
- LinkedBlockingDeque,由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素
- 对于一些指定的操作,在插入或者获取队列元素时如果队列状态不允许该操作可能会阻塞住该线程直到队列状态变更为允许操作,这里的阻塞一般有两种情况
- 插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再讲该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数一直阻塞,中断后抛出InterruptedException异常
- 读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
线程池
工作流程:
- 提交任务后,如果核心大小未满,则创建新线程处理
- 如果核心大小已满,则放入队列中,如果队列已满,则看是否超过最大线程数大小
- 如果没有超过,则新来的任务会创建新线程处理,如果最大线程数满,则执行拒绝策略
- 注意:队列满后,新来的任务会优先于队列中的进行创建线程处理(能容忍在队列中等待这么久认为可以继续让当前任务等待)
拒绝策略: - AbortPolicy,默认,直接抛出异常
- CallerRunsPolicy,调用者线程进行执行
- DiscardOldestPolicy,删掉队列中等待最久的任务,然后继续提交当前任务
- DiscardPolicy,直接丢掉当前任务
Fork/Join框架
RecursiveTask:实现任务拆分,是ForkJoinTask的子类,fork()拆分新任务,join()获取任务结果
ForkJoinPool:将ForkJoinTask提交运行,返回最终计算结果
CompletableFuture异步回调
默认使用ForkJoinPool.commonPool()线程池的线程
- CompletableFuture.runAsync()没有返回结果的异步调用
- CompletableFuture.supplyAsync()有返回结果的异步调用
接口都在CompletionStage里
本文来自博客园,作者:Bingmous,转载请注明原文链接:https://www.cnblogs.com/bingmous/p/16846196.html