一、JUC是什么
java.util.concurrent在并发编程中使用的工具类
- 进程/线程是什么:进程是一个具有独立功能的程序关于某个数据集合的一次运行活动,它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元;线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义,线程可以利用进程所拥有的资源,在引入线程的操作系统中,都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高校的提高系统多个程序间并发执行的难度。
- 进程/线程例子:使用QQ,查看进程一定有一个QQ.exe的进程,我可以用qq和A文字聊天,和B视频聊天,给C传文件,给D发一段语言,QQ支持录入信息的搜索。 大四的时候写论文,用word写论文,同时用QQ音乐放音乐,同时用QQ聊天,多个进程。 word如没有保存,停电关机,再通电后打开word可以恢复之前未保存的文档,word也会检查你的拼写,两个线程:容灾备份,语法检查。
- 并发/并行:同一时刻多个线程在访问同一个资源,多个线程对一个点;三个同学同时拿手机操作空调的制冷制热功能;多项工作一起执行,之后再汇总就是并行,晚上回去泡方便面就是并行,一边烧水一边撕调料包
- 线程的状态:NEW,(新建) RUNNABLE,(准备就绪) BLOCKED,(阻塞) WAITING,(不见不散)TIMED_WAITING,(过时不候)TERMINATED;(终结)
- wait/sleep区别:功能都是当前线程暂停,有什么区别?
wait放开手去睡,放开手里的锁
sleep握紧手去睡,醒了手里还有锁 - 三个包:java.util.concurrent;java.util.concurrent.atomic;java.util.concurrent.locks;
二、Lock接口
- 复习Synchronized
- 多线程编程模板:在高内聚低耦合的前提下: 线程 操作 资源类
- 实现步骤:创建资源类;资源类里创建同步代码块,同步方法。
- SaleTicketDemo01.java
class Ticket implements Runnable{ private int ticket=30;//空调的初始温度 低耦合 public synchronized void sale(){//高内聚 if(ticket>0){ System.out.println(Thread.currentThread().getName()+"正在售第"+ticket--+"张票"); } } @Override public void run() { for(int i=0;i<100;i++){ sale(); } } } /* * 题目:3个售票员,售出30张票 * 如何编写企业级的多线程代码?固定的编程套路+模板是: * 1.在高内聚低耦合的前提下 线程 操作 资源类 * 1.1一言不合,先创建一个资源类 * 1.2资源类里创建同步代码块,同步方法 * */ public class SaleTicketDemo01 { public static void main(String[] args) { Ticket ticket=new Ticket(); Thread t1=new Thread(ticket,"A"); Thread t2=new Thread(ticket,"B"); Thread t3=new Thread(ticket,"C"); t1.start(); t2.start(); t3.start(); } }
- Lock
- 是什么:锁实现提供了比使用同步方法和语句可以获得的更广泛的锁操作。它们允许更灵活的结构,可能具有非常不同的属性,并且可能支持多个关联的条件对象。
- Lock接口的实现 ReentrantLock可重入锁:
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
synchronized与Lock的区别
两者区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁;
3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁;
4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可判断、可公平(两者皆可)
6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。 - 创建线程方式
- 继承Thread->不提倡 java是单继承,资源宝贵,要用接口方式
- new Thread()->不提倡
- 第三种 Thread(Runnable target, String name)
- 实现runnable方法
- 新建类实现runnable接口->这种方法会新增类,有更新更好的方法
- 匿名内部类->重复代码多
new Thread(new Runnable() { @Override public void run() { } }, "your thread name").start(); 这种方法不需要创建新的类,可以new接口 - lambda表达式->最优解
new Thread(() -> { }, "your thread name").start(); 这种方法代码更简洁精炼
- 代码
/*空调的制冷制热功能都是自己实现的,只是对外提供一个接口 这就是高内聚低耦合 synchronized是关键字重粒度锁,用Lock * */ class Ticket{ private int ticket=30; Lock lock=new ReentrantLock();//,可重用锁,同一个厕所,你能用,他也能用 public void sale(){ lock.lock(); try { if(ticket>0){ System.out.println(Thread.currentThread().getName()+"正在售第"+(ticket--)+"张票"); } }catch (Exception e){ e.printStackTrace(); }finally { lock.unlock(); } } } /* * 题目:3个售票员,售出30张票 * 如何编写企业级的多线程代码?固定的编程套路+模板是: * 1.在高内聚低耦合的前提下 线程 操作 资源类 * 1.1一言不合,先创建一个资源类 * 1.2资源类里创建同步代码块,同步方法 * */ public class SaleTicketDemo01 { public static void main(String[] args) { Ticket ticket=new Ticket();//资源类 new Thread(()->{ for(int i=0;i<40;i++) ticket.sale();},"A").start(); new Thread(()->{ for(int i=0;i<40;i++) ticket.sale();},"B").start(); new Thread(()->{ for(int i=0;i<40;i++) ticket.sale();},"C").start(); } }
三、Java8之lambda表达式复习
- lambda表达式
- 什么是lambda表达式:
Lambda 是一个匿名函数,我们可以把 Lambda表达式理解为是一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使Java的语言表达能力得到了提升。
Lambda 表达式在Java 语言中引入了一个新的语法元素和操作符。这个操作符为 “->” , 该操作符被称为 Lambda 操作符或剪头操作符。它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式需要的所有参数
右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能 - 查看例子
- 要求:接口只有一个方法
如果一个接口只有一个方法,我可以把方法名省略
Foo foo = () -> {System.out.println("****hello lambda");}; - 写法:拷贝小括号(),写死右箭头->,落地大括号{...}
- 函数式接口:lambda表达式,必须是函数式接口,必须只有一个方法
如果接口只有一个方法java默认它为函数式接口。
为了正确使用Lambda表达式,需要给接口加个注解:@FunctionalInterface
如有两个方法,立刻报错
Runnable接口为什么可以用lambda表达式?因为这个接口只有一个run()
- 什么是lambda表达式:
- 接口里是否能有实现方法
- default方法:接口里在java8后容许有接口的实现,default方法默认实现,可以有多个默认方法
- 静态方法实现:静态方法实现:接口新增;可以有多个静态方法;
- 代码
@FunctionalInterface//接口只能有一个方法 interface Foo{ public int com(int a,int b); // public void sayHello(); public default int mul(int x,int y){ return x*y; } public static int div(int x,int y){ return x/y; } } public class LambdaExpressDemo02 { public static void main(String[] args) { Foo foo=new Foo() { @Override public int com(int a,int b) { return a+b; } }; System.out.println(foo.com(5,3)); Foo foo1=(int x,int y)->{return x+y;}; System.out.println(foo1.com(2,4)); System.out.println(foo1.mul(3,5)); System.out.println(Foo.div(20,2)); } }
四、线程间通信
- 面试题:两个线程打印:两个线程,一个线程打印1-52,另一个打印字母A-Z打印顺序为12A34B...5152Z,
要求用线程间通信class NumberLatter{ private int index=1; public synchronized void printNumber(int i) throws InterruptedException { while (index%3==0){ wait(); } System.out.print(i); index++; notifyAll(); } public synchronized void printLatter(char latter) throws InterruptedException { while (index%3!=0){ wait(); } System.out.print(latter); index++; notifyAll(); } } public class NotifyWaitDemo { public static void main(String[] args) { NumberLatter numberLatter=new NumberLatter(); new Thread(()->{ for (int i=1;i<=52;i++){ try { numberLatter.printNumber(i); } catch (InterruptedException e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for(char i='A';i<='Z';i++){ try { numberLatter.printLatter(i); } catch (InterruptedException e) { e.printStackTrace(); } } },"B").start(); } } - 线程间通信: 生产者+消费者;通知等待唤醒机制
- 多线程编程模板:判断/干活/通知
- synchronized实现
- 代码
class Airctions{ private int number=0; public synchronized void increment()throws Exception{ //1.判断 if(number!=0){ this.wait(); } //2.干活 number++; System.out.println(Thread.currentThread().getName()+number); //3.通知 this.notifyAll(); } public synchronized void decrement()throws Exception{ if(number==0){ wait(); } number--; System.out.println(Thread.currentThread().getName()+number); this.notifyAll(); } } /** * 现在有2个线程,可以操作初始值为0的一个变量 * 实现一个线程对变量加1,一个线程对变量减1 * 实现交替,来十轮,变量初始值为0 * 1.高内聚低耦合前提下,线程操作资源类 * 2.判断/干活/通知 */ public class CopyProdConsumerDemo04 { public static void main(String[] args) { Airctions airction=new Airctions(); for(int i=0;i<10;i++){ new Thread(()->{ try { airction.increment(); } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)+"A").start(); } for(int i=0;i<10;i++){ new Thread(()->{ try { airction.decrement(); } catch (Exception e) { e.printStackTrace(); } },String.valueOf(i)+"B").start(); } } } - 换成四个线程
public class ProdConsumerDemo04 { public static void main(String[] args) { Airction airction=new Airction(); new Thread(()->{ for (int i=0;i<10;i++){ try { airction.increment(); } catch (Exception e) { e.printStackTrace(); } } },"A").start(); new Thread(()->{ for (int i=0;i<10;i++){ try { airction.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"B").start(); new Thread(()->{ for (int i=0;i<10;i++){ try { airction.increment(); } catch (Exception e) { e.printStackTrace(); } } },"C").start(); new Thread(()->{ for (int i=0;i<10;i++){ try { airction.decrement(); } catch (Exception e) { e.printStackTrace(); } } },"D").start(); } } - 解决办法:while
class Airctions{ private int number=0; public synchronized void increment()throws Exception{ //1.判断 if(number!=0){ this.wait(); } //2.干活 number++; System.out.println(Thread.currentThread().getName()+number); //3.通知 this.notifyAll(); } public synchronized void decrement()throws Exception{ if(number==0){ wait(); } number--; System.out.println(Thread.currentThread().getName()+number); this.notifyAll(); } }
- 代码
- 多线程编程模板下
- 注意多线程之间的虚假唤醒防止多线程的虚假唤醒;全班同学包了一架飞机,过安检,飞机上收到威胁电话,全部下飞机,刚刚是误报,需不需要再过一次安检?需要
相同类型的线程执行完后,又唤醒了刚才的,没判断直接新增了?
调用wait以后就停在那个位置?然后被唤醒就接着执行,不会再去判断?
用if判断的话线程唤醒就会继续往下走,如果用while就会重新判断?
wait会释放对象锁,所以另外一个线程也会进去,JVM多线程的调度是随机的
现在是已经有一块蛋糕,+出去了,notifyAll又唤醒了一个加的线程,++进来了,if(number!=0) wait()了,在蛋糕房里wait,这时,+又进来了,蛋糕还没有消费,所以+也在蛋糕房里wait,外面只有-的两个线程了,-进去吃蛋糕,吃完了,number=0;由于是if判断,不会再重新判断,所以两个+的线程直接干活,(JVM看它两等了很久) number就成了2
- 注意多线程之间的虚假唤醒防止多线程的虚假唤醒;全班同学包了一架飞机,过安检,飞机上收到威胁电话,全部下飞机,刚刚是误报,需不需要再过一次安检?需要
- java8新版发现
- 对标实现
- Condition:一把锁要三个钥匙开
五、线程间定制化调用通信
class ShareData{
private int number=1;//A:1 B:2 C:3
private Lock lock=new ReentrantLock();
//一把锁要三把钥匙开
private Condition c1=lock.newCondition();
private Condition c2=lock.newCondition();
private Condition c3=lock.newCondition();
public void print5(){
lock.lock();
try{
while (number!=1){
c1.await();
}
for(int i=1;i<=5;i++){
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=2;
//如何通知第二个?
c2.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void print10(){
lock.lock();
try{
while (number!=2){
c2.await();
}
for(int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=3;
//如何通知第三个?
c3.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
public void print15(){
lock.lock();
try{
while (number!=3){
c3.await();
}
for(int i=1;i<=15;i++){
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
number=1;
//如何通知第二个?
c1.signal();
}catch(Exception e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
/**
* 多线程之间按顺序调用,实现A->B->C
* AA打印5次,BB打印10次,CC打印15次
* 接着
* AA打印5次,BB打印10次,CC打印15次
* 来10轮
*/
public class ConditionDemo {
public static void main(String[] args) {
ShareData shareData=new ShareData();
new Thread(()->{
for (int i=1;i<=10;i++){
shareData.print5();
}
},"A").start();
new Thread(()->{
for (int i=1;i<=10;i++){
shareData.print10();
}
},"B").start();
new Thread(()->{
for (int i=1;i<=10;i++){
shareData.print15();
}
},"C").start();
}
}
六、NotSafeDemo
- 证明集合不安全
1.故障现象 ArrayList在迭代的时候如果同时对其进行修改就会 抛出java.util.ConcurrentModificationException异常 并发修改异常 2.导致原因 多线程并发争夺同一个资源类,且没有加锁!看ArrayList的源码,没有synchronized线程不安全 public class NotSafeDemo03 { public static void main(String[] args) { List<String> list=new ArrayList<>(); for(int i=0;i<30;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8));//三个线程来访问list System.out.println(list); },String.valueOf(i)).start(); } } } List<String> list=new CopyOnWriteArrayList<>(); //Collections.synchronizedList(new ArrayList<>());//new Vector<>();//new ArrayList<>();并发修改异常:java.util.ConcurrentModificationException;同学们在签到,张三正在纸上写名字,李四过来抽了一下签到表看是否有自己的名字,然后张三就在纸上划了长长的一道,这就是并发修改异常 - 解决方案
Vector线程安全(有synchronized线程安全,并发性和数据一致性本身就是向左的,synchornized是重锁,同一时间段只有一个线程来操作)Collections.synchronizedList(new ArrayList<>());[那HashMap,HashSet是线程安全的吗?也不是,所以有同样的线程安全方法]new CopyOnWriteArrayList<>();
- 写时复制:不加锁性能提升出错误,加锁数据一致性能下降
- CopyOnWriteList定义:CopyOnWriteArrayList是arraylist的一种线程安全变体,其中所有可变操作(add、set等)都是通过生成底层数组的新副本来实现的。
- 举例:名单签到
我写的时候复制了一份,你们读你们的,我写好以后,你们依然在读的,我写好了会把上一个版本撤掉,换上这份高版本的 - CopyOnWrite理论
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(); } }CopyOnWrite容器即是写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前Object[]进行复制,复制出一个新的容器 Object[] newElements,然后把新的容器 Object[] newElements里添加元素,添加完之后,再将原容器的引用指向新的容器,这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器
- 扩展类比
- HashSet
Set<String> set = new HashSet<>();//线程不安全
Set<String> set = new CopyOnWriteArraySet<>();//线程安全
HashSet底层数据结构是什么?
HashMap ?
但HashSet的add是放一个值,而HashMap是放K、V键值对
public HashSet() {
map = new HashMap<>();
}
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
} - HashMap
- HashSet
- 代码
public class NotSafeDemo03 { public static void main(String[] args) { mapNotSafe(); } private static void mapNotSafe() { Map<String,String> map=new ConcurrentHashMap<>(); for(int i=0;i<10;i++){ new Thread(()->{ map.put(UUID.randomUUID().toString().substring(0,8),"weiwei");//三个线程来访问list System.out.println(map); },String.valueOf(i)).start(); } } private static void setNotSafe() { Set<String> set=new CopyOnWriteArraySet<>(); for(int i=0;i<10;i++){ new Thread(()->{ set.add(UUID.randomUUID().toString().substring(0,8));//三个线程来访问list System.out.println(set); },String.valueOf(i)).start(); } } private static void listNotSafe() { List<String> list=new CopyOnWriteArrayList<>(); //Collections.synchronizedList(new ArrayList<>());//new Vector<>();//new ArrayList<>(); for(int i=0;i<3;i++){ new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,8));//三个线程来访问list System.out.println(list); },String.valueOf(i)).start(); } } }
七、多线程锁
- 代码
class Phone{ public static synchronized void sendEmail() throws Exception{ TimeUnit.SECONDS.sleep(4); System.out.println("发邮件"); } public synchronized void sendSMS() throws Exception{ System.out.println("发短信"); } public void sayHello() throws Exception{ System.out.println("sayHello"); } } /** *1.标准访问,请问先打印邮件还是短信? 邮件->短信 * 假设两个人,一部手机,一个人用它来发邮件,一个人用它来发短信,哪个人(线程)先拿到手机,哪个人先执行,锁的是这个手机,不是这个方法,这个手机只能在一个人手里, * 2.邮件方法暂停4秒钟,请问先打印邮件还是短信? 邮件->短信 * 班长先进来,发邮件需要4秒钟,先等4秒钟发完邮件,再发短信; * 3.新增普通sayHello方法,请问先打印邮件还是sayHello? sayHello->邮件 * 班长拿到手机发邮件四秒钟,但是sayHello没有用synchronized,不冲突,你可以边发邮件边把充电器借给我,不是竞争同一个资源类,只是拿手机的充电器, * 4.两部手机:先打印邮件还是短信? 短信->邮件 * 邮件用的时间长,两个人之间没有竞争关系,各自用各自的手机 * 5.两个静态同步方法,同一部手机,先打印邮件还是短信?邮件->短信 * static属于全局锁class,不属于对象锁this, * 6.两个静态同步方法,两个手机,先打印邮件还是短信?邮件->短信 * 7.一个静态同步方法,一个普通同步方法,同一部手机,先打印邮件还是短信?短信->邮件 * 锁的对象不一样,一个锁的是小米手机,一个锁的是小米工厂,不冲突 * 8.一个静态同步方法,一个普通方法,2部手机,先打印邮件还是短信?sayHello->邮件 */ public class Lock8Demo05 { public static void main(String[] args) throws InterruptedException { Phone phone=new Phone(); Phone phone2=new Phone(); new Thread(()->{ try { phone.sendEmail(); } catch (Exception e) { e.printStackTrace(); } },"A").start(); Thread.sleep(100); new Thread(()->{ try { //phone.sendSMS(); //phone.sayHello() // phone2.sendSMS(); phone2.sayHello(); } catch (Exception e) { e.printStackTrace(); } },"B").start(); } } - 8锁分析
A 一个对象里面如果有多个synchronized方法,某一个时刻内,只要一个线程去调用其中的一个synchronized方法了,其它的线程都只能等待,换句话说,某一个时刻内,只能有唯一一个线程去访问这些synchronized方法
锁的是当前对象this,被锁定后,其它的线程都不能进入到当前对象的其它的synchronized方法
加个普通方法后发现和同步锁无关
换成两个对象后,不是同一把锁了,情况立刻变化。synchronized实现同步的基础:Java中的每一个对象都可以作为锁。
具体表现为以下3种形式。
对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是Synchonized括号里配置的对象当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
也就是说如果一个实例对象的非静态同步方法获取锁后,该实例对象的其他非静态同步方法必须等待获取锁的方法释放锁后才能获取锁,
可是别的实例对象的非静态同步方法因为跟该实例对象的非静态同步方法用的是不同的锁,
所以毋须等待该实例对象已获取锁的非静态同步方法释放锁就可以获取他们自己的锁。
所有的静态同步方法用的也是同一把锁——类对象本身,
这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件的。
但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,
而不管是同一个实例对象的静态同步方法之间,
还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象!
八、Callable接口
- 是什么:这是一个函数式接口,因此可以用作lambda表达式或方法引用的赋值对象。 面试题:获得多线程的方法有几种: 传统的是继承thread类和实现runnable接口,java5以后又有实现callable接口和java的线程池获得;
- 与runnable对比
- 有返回值
- 有泛型
- 抛出异常
- 怎么用
- 直接替换runnable是否可行?不可行,因为:thread类的构造方法根本没有Callable
- FutureTask实现了runnable接口,FutureTask的构造方法传入Callable接口及其实现类
- FutureTask
- 是什么: 未来的任务,用它就干一件事,异步调用
main方法就像一个冰糖葫芦,一个个方法由main串起来。
但解决不了一个问题:正常调用挂起堵塞问题 例子:
(1)老师上着课,口渴了,去买水不合适,讲课线程继续,我可以单起个线程找班长帮忙买水,水买回来了放桌上,我需要的时候再去get。
(2)4个同学,A算1+20,B算21+30,C算31*到40,D算41+50,是不是C的计算量有点大啊,FutureTask单起个线程给C计算,我先汇总ABD,最后等C计算完了再汇总C,拿到最终结果
(3)高考:会做的先做,不会的放在后面做 - 原理:
在主线程中需要执行比较耗时的操作时,但又不想阻塞主线程时,可以把这些作业交给Future对象在后台完成,
当主线程将来需要时,就可以通过Future对象获得后台作业的计算结果或者执行状态。
一般FutureTask多用于耗时的计算,主线程可以在完成自己的任务后,再去获取结果。
仅在计算完成时才能检索结果;如果计算尚未完成,则阻塞 get 方法。一旦计算完成,就不能再重新开始或取消计算。get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,
然后会返回结果或者抛出异常。
只计算一次
get方法放到最后 //提前get()主线程就会阻塞等待 - 代码
class MyThread implements Callable<Integer>{ @Override public Integer call() throws Exception { System.out.println("****come to call"); return 1024; } } public class CallableDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread myThread=new MyThread(); FutureTask futureTask=new FutureTask(myThread); Thread thread=new Thread(futureTask,"A"); thread.start(); System.out.println(futureTask.get()); } }
- 是什么: 未来的任务,用它就干一件事,异步调用
九、JUC强大的辅助类讲解
- CountDownLatch减少计数
- 原理:CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),
当计数器的值变为0时,因await方法阻塞的线程会被唤醒,继续执行。 - 代码
/** * 7个线程,6个线程不知道哪个先执行完,要求班长主线程最后关灯锁门离开 */ public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch=new CountDownLatch(6);//倒计时6 for(int i=0;i<6;i++){ new Thread(()->{ System.out.println(Thread.currentThread().getName()+"离开教室"); countDownLatch.countDown();//完一个走一个 },String.valueOf(i)).start(); } countDownLatch.await();//做减法,主线程阻塞等待,等到6变成了0才能走 System.out.println(Thread.currentThread().getName()+"班长关门关灯离开教室"); } private static void closeDoor() { for(int i=0;i<6;i++){ new Thread(()->{ System.out.println(Thread.currentThread().getName()+"离开教室"); },String.valueOf(i)).start(); } System.out.println(Thread.currentThread().getName()+"班长关门关灯离开教室"); } }
- 原理:CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,这些线程会阻塞。
- CyclicBarrier循环栅栏
- 原理:CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
线程进入屏障通过CyclicBarrier的await()方法。 - 代码
/** * 2.00开会,人到齐了再开会,有人1.50就来了,有屏障不能执行,必须等到人齐了 */ public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{ System.out.println("召唤神龙"); }); for(int i=0;i<7;i++){ final int tempInt=i; new Thread(()->{ System.out.println(Thread.currentThread().getName()+"收集到第"+tempInt+"颗龙珠"); try { cyclicBarrier.await();//做加法,等7次 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } },String.valueOf(i)).start(); } } }
- 原理:CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
- Semaphore信号灯
- 原理:在信号量上我们定义两种操作:
acquire(获取) 当一个线程调用acquire操作时,它要么通过成功获取信号量(信号量减1),
要么一直等下去,直到有线程释放信号量,或超时。
release(释放)实际上会将信号量的值加1,然后唤醒等待的线程。
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制。 - 代码
/** * 信号灯:争车位,6个人开车上班,只有3个车位,6个车都能进去车位,控制多线程的并发数 */ public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore=new Semaphore(3);//模拟资源类,3个车位 for(int i=1;i<=6;i++){ new Thread(()->{ try { semaphore.acquire();//抢占空车位 System.out.println(Thread.currentThread().getName()+"抢占到了车位"); try{TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e){e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"离开了车位"); } catch (InterruptedException e) { e.printStackTrace(); }finally { semaphore.release();//释放这个车位 } },String.valueOf(i)).start(); } } }
- 原理:在信号量上我们定义两种操作:
十、ReentrantReadWriteLock读写锁
class MyCache{
private volatile Map<String,Object> map=new HashMap<>();
private ReadWriteLock readWriteLock=new ReentrantReadWriteLock();
public void put(String key,Object value){
readWriteLock.writeLock().lock();
try{
System.out.println(Thread.currentThread().getName()+"写入数据"+key);
try{
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
map.put(key, value);
System.out.println(Thread.currentThread().getName()+"写入完成");
}catch(Exception e){
e.printStackTrace();
}finally {
readWriteLock.writeLock().unlock();
}
}
public void get(String key){
readWriteLock.readLock().lock();//读的时候不可以写
try{
System.out.println(Thread.currentThread().getName()+"读取数据");
try{TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e){e.printStackTrace();}
Object result = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取完成"+result);
}catch(Exception e){
e.printStackTrace();
}finally {
readWriteLock.readLock().unlock();
}
}
}
public class RedWriteLockDemo {
public static void main(String[] args) {
MyCache myCache=new MyCache();
for(int i=1;i<=5;i++){
final int temp=i;
new Thread(()->{
myCache.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for(int i=1;i<=5;i++){
final int temp=i;
new Thread(()->{
myCache.get(temp+"");
},String.valueOf(i)).start();
}
}
}
十一、阻塞队列
饭店为了留住客人,让客人阻塞,给客人提供游戏零食;不得不阻塞,必须要阻塞,怎么管理好阻塞人员;
- 阻塞队列
阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列线程1往阻塞队列里添加元素,线程2从阻塞队列里移除元素
当队列是空的,从队列中获取元素的操作将会被阻塞
当队列是满的,从队列中添加元素的操作将会被阻塞
试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增 - 阻塞队列的用处
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤起
为什么需要BlockingQueue
好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。 - 种类分析
ArrayBlockingQueue:由数组结构组成的有界阻塞队列。 LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列。 PriorityBlockingQueue:支持优先级排序的无界阻塞队列。 DelayQueue:使用优先级队列实现的延迟无界阻塞队列。 SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列。 LinkedTransferQueue:由链表组成的无界阻塞队列。 LinkedBlockingDeque:由链表组成的双向阻塞队列。 - 核心方法
抛出异常 当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException 特殊值 插入方法,成功ture失败false移除方法,成功返回出队列的元素,队列里没有就返回null 一直阻塞 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据or响应中断退出当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用 超时退出 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退出 - 代码
/** * 阻塞队列,一个在买蛋糕,一个在卖蛋糕,柜台就是那个队列 * 1.抛出异常 * 2.特殊值 * 3.一直阻塞 * 4.超时退出 */ public class BlockingQueueDemo { public static void main(String[] args) throws InterruptedException { //add(); //offer(); //putAndtake(); //offertime(); } private static void offertime() throws InterruptedException { /** * 当阻塞队列满时,队列会阻塞生产者线程一段时间,超过限制时间后生产者线程会退出, */ BlockingQueue blockingQueue=new ArrayBlockingQueue(3); System.out.println(blockingQueue.offer("a"));//true System.out.println(blockingQueue.offer("b"));//true System.out.println(blockingQueue.offer("c"));//true System.out.println(blockingQueue.offer("a",3L, TimeUnit.SECONDS));//等你3s还没有位置我就走 } private static void putAndtake() throws InterruptedException { /** * 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产者线程直到put数据或响应中断退出 * 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者直到队列可用 */ BlockingQueue blockingQueue=new ArrayBlockingQueue(3); blockingQueue.put("a");//什么都没有输出 blockingQueue.put("b");//什么都没有输出 blockingQueue.put("c");//什么都没有输出 // blockingQueue.put("x");//什么都没有输出,在旁边默默等待 System.out.println(blockingQueue.take());//a System.out.println(blockingQueue.take());//b System.out.println(blockingQueue.take());//c System.out.println(blockingQueue.take());//什么都没有输出 } private static void offer() { /** * 插入方法:成功true失败false * 移除方法:成功返回出队列元素,队列里没有就返回null */ BlockingQueue blockingQueue2=new ArrayBlockingQueue(3); System.out.println(blockingQueue2.offer("a"));//true System.out.println(blockingQueue2.offer("b"));//true System.out.println(blockingQueue2.offer("c"));//true // System.out.println(blockingQueue2.offer("a"));//false System.out.println(blockingQueue2.poll());//a System.out.println(blockingQueue2.poll());//b System.out.println(blockingQueue2.poll());//c System.out.println(blockingQueue2.poll());//null } private static void add() { /** * 当阻塞队列满时,再往队列里add元素会抛IllegalStateException: Queue full异常 * 当阻塞队列空时,在从队列里remove元素会抛NoSuchElementException异常 */ BlockingQueue blockingQueue1=new ArrayBlockingQueue(3);//有界 System.out.println(blockingQueue1.add("a"));//true System.out.println(blockingQueue1.add("b"));//true System.out.println(blockingQueue1.add("c"));//true System.out.println(blockingQueue1.element());//a // System.out.println(blockingQueue.add("d"));//IllegalStateException: Queue full System.out.println(blockingQueue1.remove());//a System.out.println(blockingQueue1.remove());//b System.out.println(blockingQueue1.remove());//c // System.out.println(blockingQueue.remove());//NoSuchElementException } }
十二、ThreadPool线程池
- 为什么用线程池:例子:
10年前单核CPU电脑,假的多线程,像马戏团小丑玩多个球,CPU需要来回切换。
现在是多核电脑,多个线程各自跑在独立的CPU上,不用切换效率高。
线程池的优势:
线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用;控制最大并发数;管理线程。
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。
第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 - 通俗理解:楼下的银行就是线程池,里面有5个窗口,5个线程,5个工作人员接70个工作请求
- 线程池如何使用
- 架构说明:Java中的线程池是通过Executor框架实现的,(相当于Collection)该框架中用到了Executor,Executors(数组里Arrays,Collections),ExecutorService(线程池),ThreadPoolExecutor这几个类
- 编码实现
- Executors.newFixedThreadPool(int):执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程
- Executors.newSingleThreadExecutor():一个任务一个任务的执行,一池一线程
- Executors.newCachedThreadPool():执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强
- 代码
private static void initPool() { // ExecutorService threadpool= Executors.newFixedThreadPool(5);//一个银行5个受理窗口 // ExecutorService threadpool= Executors.newSingleThreadExecutor();//一个银行1个受理窗口 ExecutorService threadpool= Executors.newCachedThreadPool();//一个银行可能今天业务暴增,人多了窗口暴增 try{ //模拟十个用户来银行办理业务,每个用户办理的业务不一样,所以execute()里面是runnable接口 for(int i=1;i<=10;i++){ threadpool.execute(()->{ try{ TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e){e.printStackTrace();} System.out.println(Thread.currentThread().getName()+"办理开卡业务"); }); //体现了线程的复用,一个窗口可以办多个业务 } }catch(Exception e){ e.printStackTrace(); }finally { threadpool.shutdown(); } }
- ThreadPoolExecutor底层原理
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
- 线程池7大参数
1、corePoolSize:线程池中的常驻核心线程数(银行最少有一个人值班) 2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1 3、keepAliveTime:多余的空闲线程的存活时间,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为止 4、unit:keepAliveTime的单位 5、workQueue:任务队列,被提交但尚未被执行的任务(三个窗口4位同学,一个同学需要等待,银行的候客区) 6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可(银行职工胸卡一致,制服一致) 7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,如何来拒绝,请求执行的runnable的策略(窗口也满了,候客区也满了,立个牌子,本银行人员已满,去别的网点) - 线程池底层工作原理:
1、在创建了线程池后,开始等待请求。 2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:
2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;(开了两个窗口,一共5个窗口,来了5个人,3个人在候客区等待,候客区也满了,开线程到最大线程数,5个正在执行的线程,候客区也满了,拒绝策略)
2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。3、当一个线程完成任务时,它会从队列中取下一个任务来执行。 4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。
所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。 - 线程池用哪个?生产中如设置合理参数
- 线程池的拒绝策略:
- 是什么:等待队列已经排满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这个时候我们就需要拒绝策略机制合理的处理这个问题。
- JDK内置的拒绝策略
AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行(线程池最多处理maxpoolsize+阻塞队列长度,超过了就报异常) CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。(谁让你找我的,你找谁) DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。(10进8,抛掉最老的) DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。 - 以上内置拒绝策略均实现了RejectedExecutionHandle接口
- 怎么样确定最大线程数maximumPoolSize?如果是CPU密集型的,就是CPU核数加1,如果是IO密集型的,就是CPU的核数除以它的阻塞系数[阻塞系数:线程花在系统IO上的时间与CPU密集任务所耗得时间比值]
- 在工作中单一的/固定数的/可变的三种创建线程池的方法哪个用的多?超级大坑:答案是一个都不用,我们工作中只能使用自定义的...Executors中JDK已经给你提供了,为什么不用?[阿里巴巴java开发手册中提到:线程池不允许使用Executors来创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险.Executors返回的线程池对象弊端如下:FixedThreadPool和SingleThreadPool.允许的请求队列长度位Integer,MAX_VALUE,可能会堆积大量的请求,从而导致OOM,CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM]
- 在工作中如何使用线程池,是否自定义过线程池
public class MyThreadPoolDemo { public static void main(String[] args) { System.out.println(Runtime.getRuntime().availableProcessors()); ExecutorService threadpool=new ThreadPoolExecutor( 2, 5, 2L,TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); try{ for(int i=1;i<=10;i++){ threadpool.execute(()->{ System.out.println(Thread.currentThread().getName()+"办理开卡业务"); }); } }catch(Exception e){ e.printStackTrace(); }finally { threadpool.shutdown(); } }
- 线程池的拒绝策略:
十三、Java8之流式计算复习
- 函数式接口
- java,util.function
- java内置核心四大函数式接口
函数式接口 参数类型 返回类型 用途 Consumer<T>
消费型接口
T void 为类型为T的对象应用操作,包含:void accept(T t) Supplier<T>
供给型接口
无 T 返回类型为T的对象,包含:T get() Function<T,R>
函数型接口
T R 对类型为T的对象应用操作,并返回结果是R类型的对象
包含:R apply(T t)
Predicate<T>
断定型接口
T boolean 确定类型为T的对象是否满足某约束.并返回boolean值,包含方法 boolean test(T t)
- java内置核心四大函数式接口
- 实例
private static void testFunction() { Function<String,Integer> function1=new Function<String, Integer>() { @Override public Integer apply(String s) { return 1024; } }; Function<String,Integer> function2=s->{return s.length();}; System.out.println(function2.apply("abc")); Predicate<String> predicate1=new Predicate<String>() { @Override public boolean test(String s) { return false; } }; Predicate<String> predicate2=s->{return s.isEmpty();}; System.out.println(predicate2.test("abc")); Consumer<String> consumer1=new Consumer<String>() { @Override public void accept(String s) { } }; Consumer<String> consumer2=s->{ System.out.println(s); }; consumer2.accept("weiwei"); Supplier<String> supplier1=new Supplier<String>() { @Override public String get() { return null; } }; Supplier<String> supplier2=()->{return "java";}; System.out.println(supplier2.get()); }
- java,util.function
- Stream流
- What:流(Stream) 到底是什么呢?
是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。
“集合讲的是数据,流讲的是计算!” - Why
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
- How
- 创建一个Stream:一个数据源(数组、集合)
- 中间操作:一个中间操作,处理数据源数据
- 终止操作:一个终止操作,执行中间操作链,产生结果
- 源头=>中间流水线=>结果[西瓜->西瓜汁]
- 代码
/** * 题目:按照给出数据,找出同时满足以下条件的用户: * 偶数ID且年龄大于24,用户名转为大写且用户字母倒排序 * 只输出一个用户名字 */ public class StreamDemo { public static void main(String[] args) { User u1=new User(11,"a",23); User u2=new User(12,"b",24); User u3=new User(13,"c",25); User u4=new User(14,"d",26); User u5=new User(16,"e",27); List<User> list= Arrays.asList(u1,u2,u3,u4,u5); list.stream().filter(u->{return u.getId()%2==0;}). filter(u->{return u.getAge()>24;}). map(u->{return u.getUserName().toUpperCase();}). sorted((o1,o2)->{return o2.compareTo(o1);}). limit(1). forEach(System.out::println); } }
- What:流(Stream) 到底是什么呢?
十四、分支合并框架
- 原理 Fork:把一个复杂任务进行分拆,大事化小 Join:把分拆任务的结果进行合并
- 相关类
- ForkJoinPool:分支合并池 类比=> 线程池
- ForkJoinTask:ForkJoinTask 类比=> FutureTask
- RecursiveTask:递归任务:继承后可以实现递归(自己调自己)调用的任务
- 实例
class MyTask extends RecursiveTask<Integer> { private static final Integer ADJUST_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)<ADJUST_VALUE){ for(int i=begin;i<=end;i++){ result=result+i; } }else { int middle=(end+begin)/2; MyTask myTask1=new MyTask (begin,middle); MyTask myTask2=new MyTask (middle,end); myTask1.fork();//递归,还是回来调compute() myTask2.fork();//递归,还是回来调compute() result=myTask1.join()+myTask2.join(); } return result; } } /** * ForkJoinPool * ForkJoinTask-->RecursiveTask * RecursiveTask */ public class ForkJoinDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { MyTask myTask=new MyTask(0,100); ForkJoinPool threadpool=new ForkJoinPool(); ForkJoinTask<Integer> forkJoinTask = threadpool.submit(myTask);//获得任务返回值 System.out.println(forkJoinTask.get()); threadpool.shutdown(); } }
十五、异步回调
/**
*有一个同学来问老师10个问题,长期霸占老师,后面有3个同学等着问老师问题,老师和三个同学商量说,把你们
* 的问题写下来给我,解决了我明天主动联系你们
*/
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception{
//异步调用,没有返回值,两个人一起去麦当劳,一个人去健身,和另外一个人说了一声就走了,没有返回值
CompletableFuture<Void> completableFuture= CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+"没有返回,update mysql ok");
});
completableFuture.get();
//异步调用,有返回值,打电话给我,问我要不要水,我要水,他过一分钟才能过来
CompletableFuture<Integer> completableFuture1=CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+"有返回,insert mysql ok");
// int age=10/0;//干坏事
return 1024;//返回正常结果
});
completableFuture1.whenComplete((t,u)->{
System.out.println("t:"+t);
System.out.println("u:"+u);
}).exceptionally(f->{//干坏事的执行路径
System.out.println("**exception:"+f.getMessage());
return 444;
}).get();
}
}