1.JUC概述
1.1什么是juc
在 Java 中,线程部分是一个重点,本篇文章说的 JUC 也是关于线程的。JUC就是 java.util .concurrent 工具包的简称。这是一个处理线程的工具包,JDK 1.5 开始出现的。
1.2 线程和进程概念
1.进程和线程
进程(Process) 是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。 在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。程序是指令、数据及其组织形式的描述,进程是程序的实体。
线程(thread) 是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
总结来说:
进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;进程——资源分配的最小单位。
线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。线程——程序执行的最小单位。
2.线程的状态
class Thread implements Runnable{
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,//1.新建
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,//2.准备就绪
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,//3.阻塞
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
* <ul>
* <li>{@link Object#wait() Object.wait} with no timeout</li>
* <li>{@link #join() Thread.join} with no timeout</li>
* <li>{@link LockSupport#park() LockSupport.park}</li>
* </ul>
*
* <p>A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called <tt>Object.wait()</tt>
* on an object is waiting for another thread to call
* <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
* that object. A thread that has called <tt>Thread.join()</tt>
* is waiting for a specified thread to terminate.
*/
WAITING,//4.等待,不见不散
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
* <ul>
* <li>{@link #sleep Thread.sleep}</li>
* <li>{@link Object#wait(long) Object.wait} with timeout</li>
* <li>{@link #join(long) Thread.join} with timeout</li>
* <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
* <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
* </ul>
*/
TIMED_WAITING,//5.等待,过时不候
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;//6.终结
}
}
3.wati和sleep
(1)sleep 是 Thread 的静态方法,wait 是 Object 的方法,任何对象实例都能调用。
(2)sleep 不会释放锁,它也不需要占用锁。wait 会释放锁,但调用它的前提是当前线程占有锁(即代码要在 synchronized 中)。
(3)它们都可以被 interrupted 方法中断。
4.并发和并行
1.串行模式
串行表示所有任务都一一按先后顺序进行。串行意味着必须先装完一车柴才能运送这车柴,只有运送到了,才能卸下这车柴,并且只有完成了这整个三个步骤,才能进行下一个步骤。
串行是一次只能取得一个任务,并执行这个任务。
2.并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。
3. 并发
并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象, 并发描述的是多进程同时运行的现象。但实际上,对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,这里的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,这是并行的概念,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会。要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:
-
可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用
队列或类似的数据结构来存放各个小任务的成果 -
可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或
异步的方式,比如只有准备好产生了事件通知才执行某个任务。 -
可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这
些小任务,这时很可能要配合多路复用才能达到较高的效率
4. 小结(重点)
并发:同一时刻多个线程在访问同一个资源,多个线程对一个点
例子:春运抢票 电商秒杀…
并行:多项工作一起执行,之后再汇总
例子:泡方便面,电水壶烧水,一边撕调料倒入桶中
5.管程
操作系统中叫监视器,java中叫锁
**管程(monitor)**是保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现).但是这样并不能保证进程以设计的顺序执行JVM 中同步是基于进入和退出管程(monitor)对象实现的,每个对象都会有一个管程(monitor)对象,管程(monitor)会随着 java 对象一同创建和销毁执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程
6.用户线程和守护线程
**用户线程:**平时用到的普通线程,自定义线程
**守护线程:**运行在后台,是一种特殊的线程,比如垃圾回收
当主线程结束后,用户线程还在运行,JVM 存活,如果没有用户线程,都是守护线程,JVM 结束
代码1:
public static void main(String[] args) {
Thread aa = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"用户线程");
while(true){
}
}, "aa");
aa.start();
System.out.println(aa.isDaemon()+"==是否是守护线程");
System.out.println(Thread.currentThread().getName()+"====over");
}
//输出如下,且服务未停止
false==是否是守护线程
main====over
aa用户线程
代码2:
public static void main(String[] args) {
Thread aa = new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"用户线程");
while(true){}
}, "aa");
aa.setDaemon(true);
aa.start();
System.out.println(aa.isDaemon()+"==是否是守护线程");
System.out.println(Thread.currentThread().getName()+"====over");
}
//输出如下,且服务已停止
true==是否是守护线程
main====over
aa用户线程
2.LOCK接口
2.1 复习Synchronized
1.Synchronized作用范围
synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
synchronized(this){
}
-
修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;o 虽然可以使用 synchronized 来定义方法,但 synchronized 并不属于方法定义的一部分,因此,synchronized 关键字不能被继承。如果在父类中的某个方法使用了synchronized 关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized 关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。
-
修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
-
修改一个类,其作用的范围是 synchronized 后面括号括起来的部分,作用主的对象是这个类的所有对象。
2.Synchronized实现卖票例子
3个售票员 卖出30张票
//1.定义资源,设置资源的操作方式
class Ticket{
private int num=30;
public synchronized void sale(){
if(num>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+num--+",剩余:"+num);
}
}
}
public class SaleTicket {
public static void main(String[] args) {
Ticket ticket = new Ticket();
//2.创建多线程操作
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<30;i++){
ticket.sale();
}
}
},"CC").start();
}
}
3.多线程编程步骤(上)
1.第一 创建资源类,创建属性和操作方法
2.第二 创建多线程调用资源类的方法
2.2 什么是Lock接口
Lock 锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。Lock 提供了比 synchronized 更多的功能。
Lock 与的 Synchronized 区别:
- Lock 不是 Java 语言内置的,synchronized 是 Java 语言的关键字,因此是内置特性。Lock 是一个类,通过这个类可以实现同步访问;
- Lock 和 synchronized 有一点非常大的不同,采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用;而 Lock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。
2.3 Lock实现可重入锁
ReentrantLock
2.4 创建线程的多种方式
-
1.继承Thread类
-
2.实现Runnable接口
-
3.使用Callable接口
-
4.使用线程池
2.5 使用Lock实现卖票
class LTicket{
private Integer num = 30;
//创建可重入锁
ReentrantLock reentrantLock = new ReentrantLock();
public void sale(){
reentrantLock.lock();
try{
if(num>0){
System.out.println(Thread.currentThread().getName()+"卖出了"+num--+"还剩下"+num);
}
}finally {
reentrantLock.unlock();
}
}
}
public class SaleTicket {
public static void main(String[] args) {
LTicket lTicket = new LTicket();
new Thread(()->{
for (int i=0;i<30;i++){
lTicket.sale();
}
},"AA").start();
new Thread(()->{
for (int i=0;i<30;i++){
lTicket.sale();
}
},"BB").start();
new Thread(()->{
for (int i=0;i<30;i++){
lTicket.sale();
}
},"CC").start();
}
}
start()方法调用时候线程是否会立刻创建?不一定,start()方法源码如下,start0()中的native java代码无能为力,指令给操作系统,具体由操作系统决定。
class Thread implements Runnable {
public synchronized void start() {
/**
* * This method is not invoked for the main method thread or "system" *
* group threads created/set up by the VM. Any new functionality added *
* to this method in the future may have to also be added to the VM. * *
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/*
* Notify the group that this thread is about to be started * so that it
* can be added to the group's list of threads * and the group's
* unstarted count can be decremented.
*/ group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/*
* do nothing. If start0 threw a Throwable then it will be
* passed up the call stack
*/ }
}
}
private native void start0();
}
Lock 和 synchronized 有以下几点不同:
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现;
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock()去释放锁,则很
可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁; - Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用synchronized 时,等待的线程会一直等待下去,不能够响应中断;4. 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
- Lock 可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源
非常激烈时(即有大量线程同时竞争),此时 Lock 的性能要远远优于
synchronized。
3.线程间通信
3.1多线程编程步骤(中)
第二步:在资源类操作方法:
1.判断
2.干活
3.通知
例:有两个线程
实现对一个初始值是0的变量
一个线程对值:+1
一个线程对值:-1
实现+1,-1,+1,-1的交替完成,这个过程就叫做线程间通信
public class Object {
public final void wait() throws InterruptedException {
wait(0);
}
public final native void notify();
public final native void notifyAll();
}
线程间通信-synchronized实现:
class Share{
//第1步:定义资源,资源的操作方法
private int num = 0;
//第2步:判断,干活,通知
//生产
public synchronized void incr() throws InterruptedException {
//2.1 判断
if(num!=0){
this.wait();
}
//2.2 干活
num++;
System.out.println(Thread.currentThread().getName()+"==="+num);
//2.3 通知
this.notifyAll();
}
//消费
public synchronized void decr() throws InterruptedException {
//2.1 判断
if(num!=1){
this.wait();
}
//2.2 干活
num--;
System.out.println(Thread.currentThread().getName()+"==="+num);
//2.3 通知
this.notifyAll();
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
new Thread(() -> {
try {
for(int i=1;i<=10;i++){
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者").start();
new Thread(() -> {
try {
for(int i=1;i<=10;i++){
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者").start();
}
}
3.2 虚假唤醒问题
给上述线程间通信-synchronized实现各自多增加一个生产者和消费者,即会出现小于0或者大于1的情况出现,究其原因在于生产者1生产后,生产者2被唤醒并抢到资源,然后wait(),后生产者1又被唤醒,生产者2又被唤醒,因此从wait()地方醒来,继续执行后续逻辑,即产生超过1的数值,消费者小于0同理。
//生产
public synchronized void incr() throws InterruptedException {
//2.1 判断
while(num!=0){
this.wait();
}
//2.2 干活
num++;
System.out.println(Thread.currentThread().getName()+"==="+num);
//2.3 通知
this.notifyAll();
}
//消费
public synchronized void decr() throws InterruptedException {
//2.1 判断
while(num!=1){
this.wait();
}
//2.2 干活
num--;
System.out.println(Thread.currentThread().getName()+"==="+num);
//2.3 通知
this.notifyAll();
}
将上述判断中的if替换为while,即完成在唤醒后的再次判断。
3.3 多线程编程步骤(下)
第四步:防止虚假唤醒问题
3.4 多线程编程步骤总结:
-
1.第一步: 创建资源类,创建属性和操作方法
-
2.第二步:在资源类操作方法:
- 1.判断
- 2.干活
- 3.通知
-
3.第三步: 创建多线程调用资源类的方法
-
4.第二步:防止虚假唤醒问题
线程间通信-lock实现:
//java.util.concurrent.locks包下Lock接口
public interface Lock {
//获取锁
void lock();
//如果当前线程未被中断,则获取锁
void lockInterruptibly() throws InterruptedException;
//仅在调用时锁为空闲状态才获取该锁
boolean tryLock();
//如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//释放锁
void unlock();
//返回绑定到此Lock实例的新Condition实例
Condition newCondition();
}
public interface Condition {
//造成当前线程在接到信号或被中断之前一直处于等待状态
void await() throws InterruptedException;
//造成当前线程在接到信号之前一直处于等待状态
void awaitUninterruptibly();
//造成当前线程在接到信号、被中断或到达等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout) throws InterruptedException;
//造成当前线程在街道信号、被中断或到达指定等待事件之前一直处于等待状态
boolean await(long time, TimeUnit unit) throws InterruptedException;
//造成当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
boolean awaitUntil(Date deadline) throws InterruptedException;
//唤醒一个等待线程
void signal();
//唤醒所有等待线程
void signalAll();
}
//1.定义资源,资源的操作
class Share{
private int num=0;
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
//2.资源操作里,判断,干活,通知
//+1
public void incr() throws InterruptedException {
lock.lock();
try {
//4.防止虚假唤醒
while(num!=0){
condition.await();
}
num++;
System.out.println(Thread.currentThread().getName()+"::"+num);
condition.signalAll();
} finally {
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try{
//4.防止虚假唤醒
while (num!=1){
condition.await();
}
num--;
System.out.println(Thread.currentThread().getName()+"::"+num);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
Share share = new Share();
//创建多线程调用资源类的方法
new Thread(() -> {
try {
for(int i=1;i<=10;i++){
share.incr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"生产者1").start();
new Thread(() -> {
try {
for(int i=1;i<=10;i++){
share.decr();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
},"消费者1").start();
}
}
4.线程间定制化通信
/**
关键字synchronized与wait()/notify()这两个方法一起使用可以实现等待/通知模式, Lock锁的newContition()方法返回Condition对象,Condition类也可以实现等待/通知模式。
用notify()通知时,JVM会随机唤醒某个等待的线程, 使用Condition类可以进行选择性通知, Condition比较常用的两个方法:
● await()会使当前线程等待,同时会释放锁,当其他线程调用signal()时,线程会重新获得锁并继续执行。
● signal()用于唤醒一个等待的线程。
注意:在调用Condition的await()/signal()方法前,也需要线程持有相关的Lock锁,调用await()后线程会释放这个锁,在singal()调用后会从当前Condition对象的等待队列中,唤醒 一个线程,唤醒的线程尝试获得锁, 一旦获得锁成功就继续执行。
**/
/**
* @author :leeliang
* @date :Created in 2021/8/1 2:30
* @description:定制化线程通信
* @modified By:
* @version: 1.0$
*/
//1.定义资源,创建属性操作方法
class ShareResource{
private int flag =1;//1 AA;2 BB;3 CC;
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
Condition c3 = lock.newCondition();
//2.在属性操作方法内:判断,干活,通知
//打印5次方法
public void print5(int loop) throws InterruptedException {
lock.lock();
//判断
try{
while(flag!=1){
c1.await();
}
//干活
for(int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"::第"+i+"次"+"=="+loop+"轮");
}
//通知BB线程
flag=2;
c2.signal();
}finally {
lock.unlock();
}
}
//打印10次方法
public void print10(int loop) throws InterruptedException {
lock.lock();
//判断
try{
while(flag!=2){
c2.await();
}
//干活
for(int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"::第"+i+"次"+"=="+loop+"轮");
}
//通知CC线程
flag=3;
c3.signal();
}finally {
lock.unlock();
}
}
//打印15次方法
public void print15(int loop) throws InterruptedException {
lock.lock();
//判断
try{
while(flag!=3){
c3.await();
}
//干活
for(int i=1;i<=15;i++){
System.out.println(Thread.currentThread().getName()+"::第"+i+"次"+"=="+loop+"轮");
}
//通知AA线程
flag=1;
c1.signal();
}finally {
lock.unlock();
}
}
}
public class CustomerThread {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(()->{
for(int i=1;i<=10;i++){
try {
shareResource.print5(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for(int i=1;i<=10;i++){
try {
shareResource.print10(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for(int i=1;i<=10;i++){
try {
shareResource.print15(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
}
}
5.集合的线程安全
5.1ArrayList集合线程不安全演示
//list集合的add方法是线程不安全的
public interface List<E> extends Collection<E> {
boolean add(E e);
}
public static void main(String[] args) {
List list = new ArrayList();
for(int i=0;i<10;i++){
new Thread(()->{
String substring = UUID.randomUUID().toString().substring(0, 8);
list.add(substring);
System.out.println(list);
}).start();
}
}
//报错:ConcurrentModificationException
解决方案1-Vector
//jdk1.0时方案
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
}
解决方案2-Collections
//返回指定列表支持的同步列表,比较久远
List list = Collections.synchronizedList(new ArrayList<>());
解决方案3-CopyOnWriteArrayList
package java.util.concurrent
//写时复制技术
List list = new CopyOnWriteArrayList();
//源码,添加为同步操作
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
5.2 HashSet线程不安全
public static void main(String[] args) {
//Set set = new HashSet();
//用CopyOnWriteArraySet替代
Set set = new CopyOnWriteArraySet();
for(int i=0;i<30;i++){
new Thread(()->{
String substring = UUID.randomUUID().toString().substring(0, 8);
set.add(substring);
System.out.println(set);
}).start();
}
}
5.3 HashMap线程不安全
public static void main(String[] args) {
//Map map = new HashMap<String,String>();
//用ConcurrentHashMap替代
Map map = new ConcurrentHashMap();
for(int i=0;i<30;i++){
String key = String.valueOf(i);
new Thread(()->{
String substring = UUID.randomUUID().toString().substring(0, 8);
map.put(key,substring);
System.out.println(map);
}).start();
}
}
6. 多线程锁
6.1演示锁的八种情况
class Phone {
public static synchronized void sendSMS() throws Exception {
//停留4秒
TimeUnit.SECONDS.sleep(4);
System.out.println("------sendSMS");
}
public synchronized void sendEmail() throws Exception {
System.out.println("------sendEmail");
}
public void getHello() {
System.out.println("------getHello");
}
}
public class Lock_8 {
public static void main(String[] args) throws Exception {
Phone phone = new Phone();
Phone phone2 = new Phone();
new Thread(() -> {
try {
phone.sendSMS();
} catch (Exception e) {
e.printStackTrace();
}
}, "AA").start();
Thread.sleep(100);
new Thread(() -> {
try {
// phone.sendEmail();
// phone.getHello();
phone2.sendEmail();
} catch (Exception e) {
e.printStackTrace();
}
}, "BB").start();
}
}
1.标准访问,先打印短信还是邮件
-----sendSMS
-----sendEmail
锁当前对象
2.停4秒在短信方法内,先打印短信还是邮件
-----sendSMS
-----sendEmail
锁当前对象
3.新增普通的hello方法,是先打短信还是hello
-----getHello
-----sendSMS
锁当前对象的同步方法
4.现在有两部手机,先打印短信还是邮件
-----sendEmail
-----sendSMS
两部手机,则为不同锁,sendSms有沉睡一秒
5.两个静态同步方法,1部手机,先打印短信还是邮件
-----sendSMS
-----sendEmail
锁的范围不是当前对象,是字节码对象Class
6.两个静态同步方法,2部手机,先打印短信还是邮件
-----sendSMS
-----sendEmail
锁的范围不是当前对象,是字节码对象Class
7.1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
-----sendEmail
-----sendSMS
锁的范围为字节码对象Class和this当前类对象
8.1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
-----sendEmail
-----sendSMS
锁的范围为字节码对象Class和this当前类对象
总结:
synchronized实现同步的基础:JAVA中的每一个对象都可以作为锁,具体表现为以下3种形式:
普通同步方法: 锁是当前实例对象
静态同步方法:锁是当前类的Class对象
同步方法快: 锁是synchronized括号里配置的对象
6.2 公平锁和非公平锁
非公平锁:线程饿死,效率高
公平锁:阳光普照,效率相对低
Lock fairSync = new ReentrantLock(false);
Lock nonfairSync = new ReentrantLock(true);
6.3 可重入锁
synchronized(隐式)和Lock(显式)都是可重入锁
//Synchronized演示可重入锁
public static void main(String[] args) {
Object o = new Object();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"外层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"中层");
synchronized (o){
System.out.println(Thread.currentThread().getName()+"内层");
}
}
},"AA").start();
}
//Lock演示可重入锁
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(()->{
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+"外层");
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+"内层");
}finally {
//注销此行代码,将影响BB线程一直等待释放锁而不能执行
//lock.unlock();
}
}finally {
lock.unlock();
}
},"AA").start();
new Thread(()->{
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+"其他");
}finally {
lock.unlock();
}
},"BB").start();
}
6.4 死锁
1 死锁原因
第一:系统资源不足
第二:进程运行推进顺序不合适
第三:资源分配不当
2 死锁模拟
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(Thread.currentThread().getName()+"拿到b");
}
}
},"AA").start();
new Thread(()->{
synchronized (b){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"持有b,等待a");
synchronized (a){
System.out.println(Thread.currentThread().getName()+"拿到a");
}
}
},"BB").start();
}
3 验证是否是死锁
-
1.jps 类似linux ps -ef
-
2.jstack jvm里自带的堆栈跟踪工具
jps -l结果:
jstack 4264结果:
7.Callable接口
callable与runnable接口比较:
- 1)是否有返回值
- 2)是否抛出异常
- 3)实现方法名称不同,一个是run方法,一个是call方法
Thread的类构造方法只有接收Runnable对象,没有接收Callable对象的。因此找一个类,既和Runnable有关系,又和Callable也有关系
- Runnable接口有实现类FutureTask
- FutureTask构造可以传递Callable
8.JUC强大的辅助类
8.1 减少计数CountDownLatch
CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法
之后的语句。
• CountDownLatch 主要有两个方法,当一个或多个线程调用 await 方法时,这些线程会阻塞
• 其它线程调用 countDown 方法会将计数器减 1(调用 countDown 方法的线程不会阻塞)
• 当计数器的值变为 0 时,因 await 方法阻塞的线程会被唤醒,继续执行
//场景: 6 个同学陆续离开教室后值班同学才可以关门。
//普通版本
public static void main(String[] args) {
for(int i=1;i<=6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号走了");
},String.valueOf(i)).start();
}
System.out.println("班长锁门了");
}
/*
输出如下:
1 号走了
3 号走了
班长锁门了
2 号走了
4 号走了
5 号走了
6 号走了
*/
//使用countDownLatch
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for(int i=1;i<=6;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号走了");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("班长锁门了");
}
/*
3 号走了
4 号走了
1 号走了
2 号走了
5 号走了
6 号走了
班长锁门了
*/
8.2 循环栅栏CyclicBarrier
CyclicBarrier 看英文单词可以看出大概就是循环阻塞的意思,在使用中CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一
次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作
//场景: 集齐 7 颗龙珠就可以召唤神龙
private final static int NUMBER=7;
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(NUMBER,()->{
System.out.println("集齐龙珠,召唤神龙。");
});
for(int i=1;i<=7;i++){
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号龙珠已经收集");
try {
cyclicBarrier.await();
System.out.println(Thread.currentThread().getName()+" 号龙珠已经消失");
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
/**
输出如下:
2 号龙珠已经收集
3 号龙珠已经收集
4 号龙珠已经收集
1 号龙珠已经收集
5 号龙珠已经收集
6 号龙珠已经收集
7 号龙珠已经收集
集齐龙珠,召唤神龙。
*/
8.3 信号灯Semaphore
Semaphore 的构造方法中传入的第一个参数是最大信号量(可以看成最大线程池),每个信号量初始化为一个最多只能分发一个许可证。使用 acquire 方
法获得许可证,release 方法释放许可。
//场景: 抢车位, 6 部汽车 3 个停车位
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for(int i=1;i<=6;i++){
new Thread(()->{
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"停车");
TimeUnit.SECONDS.sleep(new Random().nextInt(5));
semaphore.release();
System.out.println(Thread.currentThread().getName()+"开走");
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
/*
输出如下:
2停车
3停车
1停车
1开走
5停车
3开走
4停车
2开走
5开走
6停车
4开走
6开走
*/
9.ReentrantReadWriteLock读写锁
9.1 读写锁概述
悲观锁效率很低,不支持并发操作
乐观锁,支持并发并且加入version版本号进行控制
表锁:不会发生死锁
行锁:会发生死锁
读锁:共享锁,会产生死锁,因为读的时候可以修改
写锁:独占锁,会产生死锁,因为写的时候可以写多条
9.2 读写锁案例
//不加锁读写
class MyCache{
private volatile Map<String,Object> map = new HashMap();
public void put(String key,Object value){
try{
System.out.println(Thread.currentThread().getName()+"号线程正在放数");
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"号线程已经放完数");
}catch (Exception e){
e.printStackTrace();
}
}
public Object get(String key){
Object resultValue = null;
try{
System.out.println(Thread.currentThread().getName()+"号线程取数");
TimeUnit.MICROSECONDS.sleep(300);
resultValue = map.get(key);
System.out.println(Thread.currentThread().getName()+"号线程已经取数="+resultValue);
}catch (Exception e){
e.printStackTrace();
}
return resultValue;
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for(int i=1;i<=3;i++){
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for(int i=1;i<=3;i++){
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
/*
输出结果如下:
1号线程正在放数
3号线程正在放数
2号线程正在放数
1号线程取数
2号线程取数
3号线程取数
2号线程已经放完数
1号线程已经放完数
3号线程已经取数=null
1号线程已经取数=1
3号线程已经放完数
2号线程已经取数=null
Process finished with exit code 0
*/
//读写锁
class MyCache{
private volatile Map<String,Object> map = new HashMap();
//创建读写锁
ReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key,Object value){
//加写锁
rwLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"号线程正在放数");
TimeUnit.MICROSECONDS.sleep(300);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"号线程已经放完数");
}catch (Exception e){
e.printStackTrace();
}finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
public Object get(String key){
Object resultValue = null;
rwLock.readLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"号线程取数");
TimeUnit.MICROSECONDS.sleep(300);
resultValue = map.get(key);
System.out.println(Thread.currentThread().getName()+"号线程已经取数="+resultValue);
}catch (Exception e){
e.printStackTrace();
}finally{
rwLock.readLock().unlock();
}
return resultValue;
}
}
public class ThreadDemo6 {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for(int i=1;i<=3;i++){
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
for(int i=1;i<=3;i++){
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
/*
输出管理:
2号线程正在放数
2号线程已经放完数
3号线程正在放数
3号线程已经放完数
1号线程正在放数
1号线程已经放完数
1号线程取数
2号线程取数
3号线程取数
1号线程已经取数=1
2号线程已经取数=2
3号线程已经取数=3
*/
9.3 AQS概述和应用
9.4 读写锁深入
1.读写锁的演变
2.读写锁的降级:
将写入锁降级为读锁
获取写锁—>获取读锁—>释放写锁—>释放读锁
读锁 不能升级为 写锁
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
//获取写锁
writeLock.lock();
System.out.println("写锁已获取");
//获取读锁
readLock.lock();
System.out.println("读锁已获取");
//释放写锁
writeLock.unlock();
//释放读锁
readLock.lock();
/*
输出结果:
写锁已获取
读锁已获取
*/
//读锁无法升级为写锁,(读的时候不能写,写的时候可以读???)
//获取读锁
readLock.lock();
System.out.println("读锁已获取");
//获取写锁
writeLock.lock();
System.out.println("写锁已获取");
/*
读锁已获取
*/
10.BlockingQueue阻塞队列
1.阻塞队列概述
Concurrent 包中,BlockingQueue 很好的解决了多线程中,如何高效安全“传输”数据的问题。通过这些高效并且线程安全的队列类,为我们快速搭建
高质量的多线程程序带来极大的便利。本文详细介绍了 BlockingQueue 家庭中的所有成员,包括他们各自的功能以及常见使用场景。阻塞队列,顾名思义,首先它是一个队列, 通过一个共享的队列,可以使得数据由队列的一端输入,从另外一端输出;
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起为什么需要 BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue 都给你一手包办了在 concurrent 包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。多线程环境中,通过队列可以很容易实现数据共享,比如经典的“生产者”和“消费者”模型中,通过队列可以很便利地实现两者之间的数据共享。假设我们有若干生产者线程,另外又有若干个消费者线程。如果生产者线程需要把准备好的数据共享给消费者线程,利用队列的方式来传递数据,就可以很方便地解决他们之间的数据共享问题。但如果生产者和消费者在某个时间段内,万一发生数据处理速度不匹配的情况呢?理想情况下,如果生产者产出数据的速度大于消费者消费的速度,并且当生产出来的数据累积到一定程度的时候,那么生产者必须暂停等待一下(阻塞生产者线程),以便等待消费者线程把累积的数据处理完毕,反之亦然。
• 当队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列
• 当队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒
2.阻塞队列的架构
3.阻塞队列分类方法
1.ArrayBlockingQueue 由数组结构组成的有界阻塞队列
2.LinkedBlockingQueue 由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
3.DelayQueue 使用优先级队列实现的延迟无界阻塞队列
4.PriorityBlockingQueue 支持优先级排序的无界阻塞队列
5.SynchronousQueue 不存储元素的阻塞队列,也即单个元素的队列
6.LinkedTransferQueue 由链表组成的无界阻塞队列
7.LinkedBlockingDeque 由链表组成的双向阻塞队列
4.阻塞队列核心方法
//核心方法演示
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
//第一组 add remove
System.out.println(queue.add("a")); //true
System.out.println(queue.add("b")); //true
System.out.println(queue.add("c")); //true
// System.out.println(queue.add("c")); java.lang.IllegalStateException: Queue full
System.out.println(queue.remove()); //a
System.out.println(queue.remove()); //b
System.out.println(queue.remove()); //c
// System.out.println(queue.remove()); java.util.NoSuchElementException
//第二组 offer poll
System.out.println(queue.offer("a")); //true
System.out.println(queue.offer("b")); //true
System.out.println(queue.offer("c")); //true
System.out.println(queue.offer("d")); //false
System.out.println(queue.poll()); //a
System.out.println(queue.poll()); //b
System.out.println(queue.poll()); //c
System.out.println(queue.poll()); //null
//第三组 put take
queue.put("a");
queue.put("b");
queue.put("c");
//queue.put("d"); 线程阻塞
System.out.println(queue.take());
System.out.println(queue.take());
System.out.println(queue.take());
// System.out.println(queue.take()); //线程阻塞
System.out.println("=================");
//第四组 offer poll
queue.offer("a");
queue.offer("b");
queue.offer("c");
// queue.offer("d",3L,TimeUnit.SECONDS); //线程阻塞3秒后退出
System.out.println(queue.poll()); //a
System.out.println(queue.poll()); //b
System.out.println(queue.poll()); //c
// System.out.println(queue.poll(3L,TimeUnit.SECONDS)); //null 线程阻塞3秒后退出
11. ThreadPool线程池
11.1 线程池概述
线程池(Thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理
者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。
例子: 10 年前单核 CPU 电脑,假的多线程,像马戏团小丑玩多个球,CPU 需要来回切换。 现在是多核电脑,多个线程各自跑在独立的 CPU 上,不用切换
效率高。
线程池的优势: 线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,
超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:
- 降低资源消耗: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
- 提高响应速度: 当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性: 线程是稀缺资源,如果无限制的创建,不仅会销耗系 统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
11.2 线程池架构
Java 中的线程池是通过 Executor 框架实现的,该框架中用到了
Executor
Executors
ExecutorService
ThreadPoolExecutor
这几个类
11.3 线程池使用方式
1.一次N线程:Executors.newFixedThreadPool(int)
**作用:**创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。
特征:
- 线程池中的线程处于一定的量,可以很好的控制线程的并发量
- 线程可以重复被使用,在显示关闭之前,都将一直存在
- 超出一定量的线程被提交时候需在队列中等待
创建方式:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
例:银行窗口对外提供服务,窗口数不变。
2.一个任务一个任务执行,一池一线程:Executors.newSingleThreadExcutor()
**作用:**创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的newFixedThreadPool 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。
特征:
创建方式:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
3.线程池根据需求创建线程,可扩容,遇强则强:Executors.newCachedThreadPool()
**作用:**创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程.
创建方式:
- 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
- 线程池中的线程可进行缓存重复利用和回收(回收默认时间为 1 分钟)
- 当线程池中,没有可用线程,会重新创建一个线程
创建方式:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
三种线程池案例:
// ExecutorService threadPool1 = Executors.newFixedThreadPool(3); 一次N线程
// ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个任务一个任务执行
ExecutorService threadPool3 = Executors.newCachedThreadPool(); //线程池根据需求创建线程
try{
for(int i=1;i<=10;i++){
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理");
});
}
}catch (Exception e){
e.printStackTrace();
}finally{
// threadPool1.shutdown();
// threadPool2.shutdown();
threadPool3.shutdown();
}
11.4 线程池底层原理
//无论哪一种线程池,都是new ThreadPoolExecutor(七个参数);
return new ThreadPoolExecutor(null,null,null,null,null,null,null);
11.5 线程池的七个参数
int corePoolSize 核心/常驻线程数量
int maximumPoolsize 最大线程数量
long keepAliveTime 线程存活时间
TimeUnit unit 线程存活时间单位
BlockingQueue<Runnable> workQueue 阻塞队列
ThreadFactory threadFactory 线程工厂
RejectedExecutionHandler handler 拒绝策略
11.6 线程池底层工作流程
工作流程:
-
在创建了线程池后,线程池中的线程数为零
-
当调用 execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
2.3 如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4 如果队列满了且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。
-
当一个线程完成任务时,它会从队列中取下一个任务来执行
-
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
4.1 如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。
4.2 所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
拒绝策略:
注意事项:
- 项目中创建多线程时,使用常见的三种线程池创建方式,单一、可变、定长都有一定问题,原因是 FixedThreadPool 和 SingleThreadExecutor 底层都是用
LinkedBlockingQueue 实现的,这个队列最大长度为 Integer.MAX_VALUE,容易导致 OOM。所以实际生产一般自己通过 ThreadPoolExecutor 的 7 个参
数自定义线程池。 - 创建线程池推荐适用 ThreadPoolExecutor 及其 7 个参数手动创建
- corePoolSize 线程池的核心线程数
- maximumPoolSize 能容纳的最大线程数
- keepAliveTime 空闲线程存活时间
- unit 存活的时间单位
- workQueue 存放提交但未执行任务的队列
- threadFactory 创建线程的工厂类
- handler 等待队列满后的拒绝策略
- 为什么不允许适用不允许 Executors.的方式手动创建线程池,如下图
11.7 自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try{
for(int i=1;i<=8;i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理");
});
}
}catch (Exception e){
e.printStackTrace();
}finally{
threadPool.shutdown();
}
12.Fork/Join分支合并框架
12.1Fork/Join 框架简介
Fork/Join 它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。Fork/Join 框架要完成两件事情:
**Fork:**把一个复杂任务进行分拆,大事化小
**Join:**把分拆任务的结果进行合并
- 任务分割:首先 Fork/Join 框架需要把大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割
- 执行任务并合并结果:分割的子任务分别放到双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
在 Java 的 Fork/Join 框架中,使用下面类完成上述操作
- **ForkJoinTask: **我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情况下我们不需要直接集
成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:- a. RecursiveAction:用于没有返回结果的任务
- b.RecursiveTask: 用于有返回结果的任务
- ForkJoinPool: ForkJoinTask 需要通过 ForkJoinPool 来执行
- RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务
Fork/Join 框架的实现原理
ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成
ForkJoinTask 数组负责将存放以及将程序提交给 ForkJoinPool
ForkJoinWorkerThread 负责执行这些任务。
12.2 Fork/Join 架构
12.3 fork/join实现原理
1.fork方法实现原理:
当我们调用 ForkJoinTask 的 fork 方法时,程序会把任务放在 ForkJoinWorkerThread 的 pushTask 的 workQueue 中,异步地执行这个任务,然后立即返回结果。
public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
pushTask 方法把当前任务存放在 ForkJoinTask 数组队列里。然后再调用ForkJoinPool 的 signalWork()方法唤醒或创建一个工作线程来执行任务。代码如下:
final void push(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) { // ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
U.putOrderedInt(this, QTOP, s + 1);
if ((n = s - b) <= 1) {
if ((p = pool) != null)
p.signalWork(p.workQueues, this);
}
else if (n >= m)
growArray();
}
}
2.join方法实现原理
Join 方法的主要作用是阻塞当前线程并等待获取结果。让我们一起看看ForkJoinTask 的 join 方法的实现,代码如下:
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
它首先调用 doJoin 方法,通过 doJoin()方法得到当前任务的状态来判断返回什么结果,任务状态有 4 种:
已完成(NORMAL)
被取消(CANCELLED)
信号(SIGNAL)
异常(EXCEPTIONAL)
- 如果任务状态是已完成,则直接返回任务结果。
- 如果任务状态是被取消,则直接抛出 CancellationException
- 如果任务状态是抛出异常,则直接抛出对应的异常
让我们分析一下dojoin方法:
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0L) :
externalAwaitDone();
}
final int doExec() {
int s; boolean completed;
if ((s = status) >= 0) {
try {
completed = exec();
} catch (Throwable rex) {
return setExceptionalCompletion(rex);
}
if (completed)
s = setCompletion(NORMAL);
}
return s;
}
在 doJoin()方法流程如下:
-
1.首先通过查看任务的状态,看任务是否已经执行完成,如果执行完成,则直接返回任务状态;
-
2.如果没有执行完,则从任务数组里取出任务并执行。
-
3.如果任务顺利执行完成,则设置任务状态为 NORMAL,如果出现异常,则记录异常,并将任务状态设置为 EXCEPTIONAL。
3.Fork/Join 框架的异常处理
ForkJoinTask 在执行的时候可能会抛出异常,但是我们没办法在主线程里直接捕获异常,所以 ForkJoinTask 提供了 isCompletedAbnormally()方法来检查
任务是否已经抛出异常或已经被取消了,并且可以通过 ForkJoinTask 的getException 方法获取异常。getException 方法返回 Throwable 对象,如果任务被取消了则返回CancellationException。如果任务没有完成或者没有抛出异常则返回 null。
12.4入门案例
场景: 生成一个计算任务,计算 1+2+3…+100,每 10 个数切分一个子任务
/**
* 分支合并案例
*/
class MyTask extends RecursiveTask<Integer> {
//拆分差值不能超过10
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() {
//判断两个数的差值是否大于10
if(end-begin<=VALUE){
//累加
for(int i=begin;i<=end;i++){
result+=i;
}
}else{
//进一步拆分
int middle = (begin+end)/2;
//拆分左边
MyTask leftTask = new MyTask(begin, middle);
//拆分右边
MyTask rightTask = new MyTask(middle+1, end);
//调用拆分方法
leftTask.fork();
rightTask.fork();
//合并结果
result = leftTask.join() + rightTask.join();
}
return result;
}
}
public class TaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//1.创建MyTask对象
MyTask myTask = new MyTask(1, 100);
//2.创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> submit = forkJoinPool.submit(myTask);
//3.获取最终得到的结果
Integer result = submit.get();
System.out.println(result);
}
}
13. CompletableFuture异步回调
13.1CompletableFuture 简介
CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息。
CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类。
13.2 Future 与 CompletableFuture
Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成。
Future 的主要缺点如下:
(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
(2)不支持进一步的非阻塞调用通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的。
13.3 CompletableFuture 入门
1)使用 CompletableFuture
**场景:**主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会阻塞,最后我们在一个子线程中使其终止。
/**
* 主线程里面创建一个 CompletableFuture,然后主线程调用 get 方法会阻塞,最后我们
在一个子线程中使其终止
* @param args
*/
public static void main(String[] args) throws Exception{
CompletableFuture<String> future = new CompletableFuture<>();
new Thread(() -> {
try{
System.out.println(Thread.currentThread().getName() + "子线程开始干活");
//子线程睡 5 秒
Thread.sleep(5000);
//在子线程中完成主线程
future.complete("success");
}catch (Exception e){
e.printStackTrace();
}
}, "A").start();
//主线程调用 get 方法阻塞
System.out.println("主线程调用 get 方法获取结果为: " + future.get());
System.out.println("主线程完成,阻塞结束!!!!!!");
}
2)没有返回值的异步任务
/**
* 没有返回值的异步任务
* @param args
*/
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
//运行一个没有返回值的异步任务
CompletableFuture<Void> future = CompletableFuture.runAsync(()-> {
try {
System.out.println("子线程启动干活");
Thread.sleep(5000);
System.out.println("子线程完成");
} catch (Exception e) {
e.printStackTrace();
}
});
//主线程阻塞
future.get();
System.out.println("主线程结束");
}
3)有返回值的异步任务
/**
* 没有返回值的异步任务
* @param args
*/
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
//运行一个有返回值的异步任务
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
try {
System.out.println("子线程开始任务");
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
return "子线程完成了!";
});
//主线程阻塞
String s = future.get();
System.out.println("主线程结束, 子线程的结果为:" + s);
}
4)线程依赖
当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。
private static Integer num = 10;
/**
* 先对一个数加 10,然后取平方
* @param args
*/
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {
try {
System.out.println("加 10 任务开始");
num += 10;
} catch (Exception e) {
e.printStackTrace();
}
return num;
}).thenApply(integer -> {
return num * num;
});
Integer integer = future.get();
System.out.println("主线程结束, 子线程的结果为:" + integer);
}
5)消费处理结果
thenAccept 消费处理结果, 接收任务的处理结果,并消费处理,无返回结果。
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture.supplyAsync(() -> {
try {
System.out.println("加 10 任务开始");
num += 10;
} catch (Exception e) {
e.printStackTrace();
}
return num;
}).thenApply(integer -> {
return num * num;
}).thenAccept(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
System.out.println("子线程全部处理完成,最后调用了 accept,结果为:" +
integer);
}
});
}
6)异常处理
exceptionally 异常处理,出现异常时触发
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
int i= 1/0;
System.out.println("加 10 任务开始");
num += 10;
return num;
}).exceptionally(ex -> {
System.out.println(ex.getMessage());
return -1;
});
System.out.println(future.get());
}
handle 类似于 thenAccept/thenRun 方法,是最后一步的处理调用,但是同时可以处理异常
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
System.out.println("加 10 任务开始");
num += 10;
return num;
}).handle((i,ex) ->{
System.out.println("进入 handle 方法");
if(ex != null){
System.out.println("发生了异常,内容为:" + ex.getMessage());
return -1;
}else{
System.out.println("正常完成,内容为: " + i);
return i;
}});
System.out.println(future.get());
}
7)结果合并
thenCompose 合并两个有依赖关系的 CompletableFutures 的执行结果
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
//第一步加 10
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(()->{
System.out.println("加 10 任务开始");
num += 10;
return num;
});
//合并
CompletableFuture<Integer> future1 = future.thenCompose(i->
//再来一个 CompletableFuture
CompletableFuture.supplyAsync(() -> {
return i + 1;
}));
System.out.println(future.get());
System.out.println(future1.get());
}
thenCombine 合并两个没有依赖关系的 CompletableFutures 任务
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
System.out.println("加 10 任务开始");
num += 10;
return num;
});
CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {System.out.println("乘以 10 任务开始");
num = num * 10;
return num;
});
//合并两个结果
CompletableFuture<Object> future = job1.thenCombine(job2, new
BiFunction<Integer, Integer, List<Integer>>() {
@Override
public List<Integer> apply(Integer a, Integer b) {
List<Integer> list = new ArrayList<>();
list.add(a);
list.add(b);
return list;
}
});
System.out.println("合并结果为:" + future.get());
}
合并多个任务的结果 allOf 与 anyOf
allOf: 一系列独立的 future 任务,等其所有的任务执行完后做一些事情
/**
* 先对一个数加 10,然后取平方
* @param args
*/
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
List<CompletableFuture> list = new ArrayList<>();
CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
System.out.println("加 10 任务开始");
num += 10;
return num;
});
list.add(job1);
CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {
System.out.println("乘以 10 任务开始");num = num * 10;
return num;
});
list.add(job2);
CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> {
System.out.println("减以 10 任务开始");
num = num * 10;
return num;
});
list.add(job3);
CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> {
System.out.println("除以 10 任务开始");
num = num * 10;
return num;
});
list.add(job4);
//多任务合并
List<Integer> collect =
list.stream().map(CompletableFuture<Integer>::join).collect(Collectors.toList());
System.out.println(collect);
}
anyOf: 只要在多个 future 里面有一个返回,整个任务就可以结束,而不需要等到每一个future 结束
/**
* 先对一个数加 10,然后取平方
* @param args
*/
public static void main(String[] args) throws Exception{
System.out.println("主线程开始");
CompletableFuture<Integer>[] futures = new CompletableFuture[4];
CompletableFuture<Integer> job1 = CompletableFuture.supplyAsync(() -> {
try{
Thread.sleep(5000);
System.out.println("加 10 任务开始");num += 10;
return num;
}catch (Exception e){
return 0;
}
});
futures[0] = job1;
CompletableFuture<Integer> job2 = CompletableFuture.supplyAsync(() -> {
try{
Thread.sleep(2000);
System.out.println("乘以 10 任务开始");
num = num * 10;
return num;
}catch (Exception e){
return 1;
}
});
futures[1] = job2;
CompletableFuture<Integer> job3 = CompletableFuture.supplyAsync(() -> {
try{
Thread.sleep(3000);
System.out.println("减以 10 任务开始");
num = num * 10;
return num;
}catch (Exception e){
return 2;
}
});
futures[2] = job3;
CompletableFuture<Integer> job4 = CompletableFuture.supplyAsync(() -> {
try{
Thread.sleep(4000);
System.out.println("除以 10 任务开始");num = num * 10;
return num;
}catch (Exception e){
return 3;
}
});
futures[3] = job4;
CompletableFuture<Object> future = CompletableFuture.anyOf(futures);
System.out.println(future.get());
}