文章目录
0. 前言
- JUC是什么
- java.util.concurrent 在并发编程中使用的工具类
- 进程和线程
- 进程:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本 单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。 线程:通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。
- 线程 可以利用进程所拥有的资源,在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为 独立运行和独立调度的基本单位,由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会 小得多,能更高效的提高系统多个程序间并发执行的程度。
1. Lock接口
1.1 复习synchronized
- 结合synchronized多线程的编程步骤主要是:(高内聚低耦合)
//第一步 创建资源类,定义属性和和操作方法
class Ticket {
//票数
private int number = 30;
//操作方法:卖票
public synchronized void sale() {
//判断:是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" : 卖出:"+(number--)+" 剩下:"+number);
}
}
}
- 创建资源类,在资源类中船舰属性和操作方法
public class SaleTicket {
//第二步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
//创建Ticket对象
Ticket ticket = new Ticket();
//创建三个线程
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"AA").start();
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"BB").start();
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"CC").start();
}
}
- 创建多个线程,调用资源类的操作方法
- 方法
- 创建多个线程,调用资源类的操作方法
new Thread(new Runnable() {
@Override
public void run() {
//调用卖票方法
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"AA").start();
- 或者可以通过lambda表达式这样创建
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
}
},"AA").start();
1.2 Lock接口
- 为锁和等待条件提供一个框架的接口和类,
不同于内置同步和监视器, LOCK是类
,可通过类实现同步访问,多个接口实现类:可重入锁等 - lock的编程步骤同synchronized
- 创建资源类,在资源类中船舰属性和操作方法
- 创建多个线程,调用资源类的操作方法
- 可重入锁的代码定义
private final ReentrantLock lock = new ReentrantLock(true);
- 上锁
lock.lock();
- 解锁
lock.unlock();
- 具体代码示列
//第一步 创建资源类,定义属性和和操作方法
class LTicket {
//票数量
private int number = 30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock(true);
//卖票方法
public void sale() {
//上锁
lock.lock();
try {
//判断是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
}
} finally {
//解锁
lock.unlock();
}
}
}
public class LSaleTicket {
//第二步 创建多个线程,调用资源类的操作方法
//创建三个线程
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
1.3 Lock方法
- lock常用接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
- lock()方法用来获取锁
- 如果锁已被其他线程获取,则进行等待
- 发生异常不会自动解锁,需用在 try{}catch{}块中进行
lock.lock();
try{
//处理任务
}catch(Exception ex){
}finally{
lock.unlock(); //释放锁
}
- Condition 类也可以实现等待/通知模式
- 关键字
synchronized 与 wait()/notify()
这两个方法一起使用可以实现等待/通知模式, - Lock 锁的 newContition()方法返回 Condition 对象,Condition 类也可以实现等待/通知模式
- 用 notify()通知时,JVM 会随机唤醒某个等待的线程, 使用 Condition 类可以进行选择性通知, Condition 比较常用的两个方法:
- await()会使当前线程等待,同时会释放锁,当其他线程调用 signal()时,线程会重新获得锁并继续执行
- signal()用于唤醒一个等待的线程
- 关键字
- ReentrantLock 是唯一实现了 Lock 接口的类,并且 ReentrantLock 提供了更多的方法
ReentrantLock可重入锁
- ReentrantReadWriteLock 里面提供了很多丰富的方法,不过最主要的有两个方法:readLock()和 writeLock()用来获取读锁和写锁
writeLock();来获取读锁
readLock();获取写锁
- 假设有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁,反之同理
1.4 synchronized与lock的异同
- synchronized
是java关键字,内置
,而lock不是内置,是一个类
,可以实现同步访问且比synchronized中的方法更加丰富
synchronized不会手动释放锁
,而lock需手动释放锁(不解锁会出现死锁,需要在 finally 块中释放锁
)lock等待锁的线程会相应中断
,而synchronized不会响应中断
,只会一直等待- 通过
Lock 可以知道有没有成功获取锁
,而 synchronized 却无法办到
5.Lock
可以提高多个线程进行读操作的效率(当多个线程竞争的时候)
2. 线程间通信
- 线程间通信的模型有两种:共享内存和消息传递
- 线程间的通信具体步骤:(涉及上中下部)
- 创建资源类,在资源类中船舰属性和操作方法
- 在资源类操作方法:判断、操作、通知
- 创建多个线程,调用资源类的操作方法
- 防止虚拟唤醒问题
2.1 synchronized案例
- 操作线程的时候,等待线程使用wait()
- 通知另外的线程操作用notify()、notifyAll()
- 假设有两个线程,该线程在执行过程中,判断值(不是该值等待,让其他线程抢),操作值,通知另外一个线程的调度
- 通过使用两个线程对0这个值操作,一个线程加1,一个线程减1,交替实现多次
//第一步 创建资源类,定义属性和操作方法
class Share {
//初始值
private int number = 0;
//+1的方法
public synchronized void incr() throws InterruptedException {
//第二步 判断 干活 通知
if(number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
//如果number值是0,就+1操作
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
//-1的方法
public synchronized void decr() throws InterruptedException {
//判断
if(number != 1) {
this.wait();
}
//干活
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知其他线程
this.notifyAll();
}
}
public class ThreadDemo1 {
//第三步 创建多个线程,调用资源类的操作方法
public static void main(String[] args) {
Share share = new Share();
//创建线程
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
}
}
- 如果使用多个线程,添加额外两个线程,且操作要依次执行
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr(); //+1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr(); //-1
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
虚拟唤醒问题
:
- 如果一个线程执行完毕后,通知其他线程,该线程又进入等待睡眠,可能会因为某些原因被唤醒后,if结构的语句就不会判断了,一直往下执行,
- 所以需要将if换成while结构,每次都判断。因为wait在哪里睡眠就在哪里被唤醒,结果被某个异常唤醒了后回不去了,if结构不会在判断了,需要更改为while
while(number != 0) { //判断number值是否是0,如果不是0,等待
this.wait(); //在哪里睡,就在哪里醒
}
2.2 Lock案例
- 使用lock先要创建锁的对象以及通知的对象
- 放置在资源类中
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
- 上锁lock.lock();
- 解锁lock.unlock();
- 以下都为condition类:
- 唤醒所有等待的线程signalAll(),带上类名condition.signalAll();
- 唤醒一个等待线程signal(),带上类名,condition.signal();
- 造成当前线程在接到信号或者被中断之前一直处于等待状态await(),带上类名,condition.await();
- lock实现
//第一步 创建资源类,定义属性和操作方法
class Share {
private int number = 0;
//创建Lock
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
//+1
public void incr() throws InterruptedException {
//上锁
lock.lock();
try {
//判断
while (number != 0) {
condition.await();
}
//干活
number++;
System.out.println(Thread.currentThread().getName()+" :: "+number);
//通知
condition.signalAll();
}finally {
//解锁
lock.unlock();
}
}
//-1
public void decr() throws InterruptedException {
lock.lock();
try {
while(number != 1) {
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName()+" :: "+number);
condition.signalAll();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo2 {
public static void main(String[] args) {
Share share = new Share();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"AA").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"BB").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.incr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"CC").start();
new Thread(()->{
for (int i = 1; i <=10; i++) {
try {
share.decr();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"DD").start();
}
}
3. 线程间定制化通信
- 所谓定制化通信,需要让线程进行一定的顺序操作
- 案列:启动三个线程,按照如下要求:
AA打印5此,BB打印10次,CC打印15次,一共进行10轮
- 具体思路:每个线程添加一个标志位,是该标志位则执行操作,并且修改为下一个标志位,通知下一个标志位的线程
- 创建一个可重入锁private Lock lock = new ReentrantLock();
- 分别创建三个开锁通知private Condition c1 = lock.newCondition();
- 上锁,(执行具体操作(判断、操作、通知),解锁)放于try、finally
- 代码
//第一步 创建资源类
class ShareResource {
//定义标志位
private int flag = 1; // 1 AA 2 BB 3 CC
//创建Lock锁
private Lock lock = new ReentrantLock();
//创建三个condition
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
//打印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);
}
//通知
flag = 2; //修改标志位 2
c2.signal(); //通知BB线程
}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);
}
//修改标志位
flag = 3;
//通知CC线程
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);
}
//修改标志位
flag = 1;
//通知AA线程
c1.signal();
}finally {
lock.unlock();
}
}
}
public class ThreadDemo3 {
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();
}
}
4. 集合的线程安全
4.0 List集合线程不安全
- 创建集合使用string的泛型
//创建ArrayList集合
List<String> list = new ArrayList<>();
- 并且再集合中添加元素获取元素
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
- 查看源码,主要牵扯这个添加的方法是因为该方法在列表中是不安全的,没有synchronized声明
boolean add(E e);
4.1 Vector
通过list下的实现类Vector
因为在Vector下的add普遍都是线程安全
代码
public synchronized boolean add(E e) {
modCount++;
add(e, elementData, elementCount);
return true;
}
在改变代码时,只需要将其修改为List<String> list = new Vector<>();
但此方法用的比较少,因为在jdk 1.0的版本适用
4.2 Collections
Collections类中的很多方法都是static静态
其中有一个方法是返回指定列表支持的同步(线程安全的)列表为 synchronizedList(List <T> list)
具体代码为
List<String> list = Collections.synchronizedList(new ArrayList<>());
此方法也比较古老,很少使用
4.3 CopyOnWriteArrayList
- 将其代码修改为
List<String> list = new CopyOnWriteArrayList<>();
- 涉及的底层原理为写时复制技术
- 读的时候并发(多个线程操作)
- 写的时候独立,
先复制相同的空间到某个区域,将其写到新区域,旧新合并,并且读新区域
(每次加新内容都写到新区域,覆盖合并之前旧区域,读取新区域添加的内容)
4.4 CopyOnWriteArraySet
该类是HashSet的实现类
同样使用HashSet类,也会出现线程不安全
Set<String> set = new HashSet<>();
需要将上面的代码改为
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
set.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(set);
},String.valueOf(i)).start();
}
4.5 ConcurrentHashMap
- ConcurrentHashMap类是HashMap的实现类
- HashMap不安全线程也同理Map<String,String> map = new HashMap<>();
具体实现代码是
for (int i = 0; i <30; i++) {
String key = String.valueOf(i);
new Thread(()->{
//向集合添加内容
map.put(key,UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(map);
},String.valueOf(i)).start();
}
将其代码修改为
Map<String,String> map = new ConcurrentHashMap<>();
通过这行代码可以编程线程安全
5. 多线程锁
- 某一个时刻内,只能有唯一一个线程去访问这些synchronized 方法
- 所有的
静态同步方法用的也是同一把锁——类对象本身
,这两把锁是两个不同的对象,所以静态同步方法与非静态同步方法之间是不会有竞态条件
的但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁,而不管是同一个实例对象的静态同步方法之间,还是不同的实例对象的静态同步方法之间,只要它们同一个类的实例对象 - 通过具体的实例进行分析
class Phone {
public 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
5 两个静态同步方法,1部手机,先打印短信还是邮件
------sendSMS
------sendEmail
6 两个静态同步方法,2部手机,先打印短信还是邮件
------sendSMS
------sendEmail
7 1个静态同步方法,1个普通同步方法,1部手机,先打印短信还是邮件
------sendEmail
------sendSMS
8 1个静态同步方法,1个普通同步方法,2部手机,先打印短信还是邮件
------sendEmail
------sendSMS
- 总结:
- 同样的对象访问不同的同步锁,是按照顺序执行
- 同样的对象访问同步锁与不同步锁,是先不同步锁执行
- 不同对象访问不同同步锁,按照顺序执行
- 同一对象访问不同静态同步锁,按照顺序执行
- 不同对象访问不同静态同步锁,按照顺序执行
- 同一对象访问一个静态同步锁,一个同步锁,先执行同步锁
- 不同对象访问一个静态同步锁,一个同步锁,先执行同步锁
- 即先出同步锁在出静态同步锁
5.1 公平锁和非公平锁
- 公平锁:效率相对低
- 非公平锁:效率高,但是线程容易饿死
- 通过查看源码
- 带有参数的ReentrantLock(true)为公平锁
- ReentrantLock(false)为非公平锁
- 主要是调用NonfairSync()与FairSync()
- 示例
//第一步 创建资源类,定义属性和和操作方法
class LTicket {
//票数量
private int number = 30;
//创建可重入锁
private final ReentrantLock lock = new ReentrantLock(true);
//卖票方法
public void sale() {
//上锁
lock.lock();
try {
//判断是否有票
if(number > 0) {
System.out.println(Thread.currentThread().getName()+" :卖出"+(number--)+" 剩余:"+number);
}
} finally {
//解锁
lock.unlock();
}
}
}
public class LSaleTicket {
//第二步 创建多个线程,调用资源类的操作方法
//创建三个线程
public static void main(String[] args) {
LTicket ticket = new LTicket();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"AA").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"BB").start();
new Thread(()-> {
for (int i = 0; i < 40; i++) {
ticket.sale();
}
},"CC").start();
}
}
都是A线程执行,而BC线程都没执行到,出现了非公平锁
具体改变其设置可以通过可重入锁中的一个有参构造方法
修改代码为private final ReentrantLock lock = new ReentrantLock(true);
代码截图为
5.2 可重入锁
-
synchronized和lock都是可重入锁
-
sychronized是隐式锁,不用手工上锁与解锁,而lock为显示锁,需要手工上锁与解锁
-
可重入锁也叫递归锁
-
而且有了可重入锁之后,破解第一把之后就可以一直进入到内层结构
-
sychronized
- 代码
- synchronized (o)代表锁住当前{ }内的代码块
- 死锁
-
lock
- 代码
- 在同一把锁中的嵌套锁,内部嵌套锁没解锁还是可以输出,但是如果跳出该线程,执行另外一个线程就会造成死锁
- 要把上锁与解锁,都要写上
5.3 死锁
- 两个或以上的进程因为争夺资源而造成互相等待资源的现象称为死锁
- 产生死锁的原因:
- 系统资源不足
- 系统资源分配不当
- 进程运行顺序不当
- 验证是否是死锁
- jps 类似于linux中的ps -ef查看进程号
- jstack jvm自带的堆栈跟踪工具
- 通过用idea自带的命令行输入 jps -l 查看其编译代码的进程号后jstack 进程号
- 死锁验证截图
- 具体死锁的操作代码实列 可理解背下来,大厂面试可考,死锁的简单案例
public class DeadLock {
//创建两个对象
static Object a = new Object();
static Object b = new Object();
public static void main(String[] args) {
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");
}
}
},"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(Thread.currentThread().getName()+" 获取锁a");
}
}
},"B").start();
}
}
6. Callable接口
- 创建线程的多种方式:
- 继承Thread类
- 实现Runnable接口
- Callable接口
- 线程池
- 目前学习了有两种创建线程的方法,一种是通过创建 Thread 类,另一种是通过使用 Runnable 创建线程,但是,Runnable 缺少的一项功能是,
当线程终止时(即 run()完成时),我们无法使线程返回结果。为了支持此功能,Java 中提供了 Callable 接口
- 比较Runnable接口和Callable接口
- Callable中的call()计算结果,如果无法计算结果,会抛出异常
- Runnable中的run()使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用该对象的run方法
- 总的来说:
run()没有返回值,不会抛出异常。而call()有返回值,会抛出异常
- 两个接口代码
//实现Runnable接口
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
return 200;
}
}
具体在主函数中
通过Thread线程创建接口
只有这个可以new Thread(new MyThread1(),"AA").start();
这个不可以new Thread(new MyThread2(),"BB").start();
因为Thread的构造函数中没有Callable接口的参数设置
直接替换不可以,只能用下面这种线程创建方法
(找一个类,即和Runnable接口有关系,又和Callable接口有关系)
发现Runnable接口有实现类FutureTask(中间对象)
FutureTask的构造函数有Callable参数,通过FutureTask创建线程对象
6.1 FutureTask
- FutureTask的构造方法有
FutureTask(Callable<> callable)
创建一个FutureTask,一旦运行就执行给定的CallableFutureTask(Runnable runnable,V result)
创建一个FutureTask,一旦运行就执行给定的Runnable你那边了,并安排成功完成时get返回给定的结果get()
获取结果isDone()
判断是否计算结束
- FutureTask 具体实现方法可以有两种方式(此处用的泛型参数)
- 一种
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
return 200;
}
}
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());
- 二种
//lambda表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
return 1024;
});
- 创建线程
new Thread(futureTask2,"lucy").start();
- 获取结果
System.out.println(futureTask2.get());
- 所谓的FutureTask是在
不影响主任务的同时,开启单线程完成某个特别的任务,之后主线程续上单线程的结果即可
(该单线程汇总给主线程只需要一次即可) 如果之后主线程在开启该单线程,可以直接获得结果,因为之前已经执行过一次了
//比较两个接口
//实现Runnable接口
class MyThread1 implements Runnable {
@Override
public void run() {
}
}
//实现Callable接口
class MyThread2 implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName()+" come in callable");
return 200;
}
}
public class Demo1 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//Runnable接口创建线程
new Thread(new MyThread1(),"AA").start();
//Callable接口,报错
// new Thread(new MyThread2(),"BB").start();
//FutureTask
FutureTask<Integer> futureTask1 = new FutureTask<>(new MyThread2());
//lam表达式
FutureTask<Integer> futureTask2 = new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName()+" come in callable");
return 1024;
});
//创建一个线程
new Thread(futureTask2,"lucy").start();
new Thread(futureTask1,"mary").start();
// while(!futureTask2.isDone()) {
// System.out.println("wait.....");
// }
//调用FutureTask的get方法
System.out.println(futureTask2.get());
System.out.println(futureTask1.get());
System.out.println(Thread.currentThread().getName()+" come over");
}
}
7. JUC强大辅助类
- 该辅助类主要讲述三个
- 分别为:
- 减少计数
CountDownLatch
- 循环栅栏
CyclicBarrier
- 信号灯
Semaphore
7.1 CountDownLatch
- 该类的构造方法为
CountDownLatch(int count)
构造一个用给定计数初始化的CountDownLatch在这里插入代码片 - 两个常用的主要方法
- await() 使当前线程在锁存器倒计数至零之前一直在等待,除非线程被中断
- countDown()递减锁存器的计数,如果计数达到零,将释放所有等待的线程
- CountDownLatch 类可以设置一个计数器,然后通过 countDown 方法来进行减 1 的操作,使用 await 方法等待计数器不大于 0,然后继续执行 await 方法之后的语句
- 具体步骤可以演化为定义一个类,减1操作,并等待到0,为0执行结果
- 通过具体的案例进行加深代码
- 6个同学陆续离开教室之后,班长才能锁门
- 如果不加 CountDownLatch类,会出现线程混乱执行,同学还未离开教室班长就已经锁门了
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {
//6个同学陆续离开教室之后
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
},String.valueOf(i)).start();
}
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}
}
具体正确的案例代码
//演示 CountDownLatch
public class CountDownLatchDemo {
//6个同学陆续离开教室之后,班长锁门
public static void main(String[] args) throws InterruptedException {
//创建CountDownLatch对象,设置初始值
CountDownLatch countDownLatch = new CountDownLatch(6);
//6个同学陆续离开教室之后
for (int i = 1; i <=6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" 号同学离开了教室");
//计数 -1
countDownLatch.countDown();
},String.valueOf(i)).start();
}
//等待
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+" 班长锁门走人了");
}
}
7.2 CyclicBarrier
- 该类是一个同步辅助类,允许一组线程互相等到,直到到达某个公共屏障点,在设计一组固定大小的线程的程序中,这些线程必须互相等待,这个类很有用,因为barrier在释放等待线程后可以重用,所以称为循环barrier
- 常用的构造方法有:
CyclicBarrier(int parties,Runnable barrierAction)
创建一个新的CyclicBarrier,它将在给定数量的参与者(线程)处于等待状态时启动,- 在启动barrier时执行给定的屏障操作,该操作由最后一个进入barrier的线程操作
- 常用的方法有:
- await()在所有的参与者都已经在此barrier上调用await方法之前一直等待
- 通过具体案例
集齐7颗龙珠就可以召唤神龙
完整代码
//集齐7颗龙珠就可以召唤神龙
public class CyclicBarrierDemo {
//创建固定值
private static final int NUMBER = 7;
public static void main(String[] args) {
//创建CyclicBarrier
CyclicBarrier cyclicBarrier =
new CyclicBarrier(NUMBER,()->{
System.out.println("*****集齐7颗龙珠就可以召唤神龙");
});
//集齐七颗龙珠过程
for (int i = 1; i <=7; i++) {
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+" 星龙被收集到了");
//等待
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
},String.valueOf(i)).start();
}
}
}
·CyclicBarrier 的构造方法第一个参数是目标障碍数,每次执行 CyclicBarrier 一次障碍数会加一,如果达到了目标障碍数,才会执行 cyclicBarrier.await()之后的语句。可以将 CyclicBarrier 理解为加 1 操作·
7.3 Semaphore
- 一个计数信号量,从概念上将,信号量维护了一个许可集,如有必要, 在许可可用前会阻塞每一个acquire(),然后在获取该许可。每个release()添加一个许可,从而可能释放一个正在阻塞的获取者 。但是,不使用实际的许可对象,Semaphore只对可用许可的号码进行计数,并采取相应的行动
- 具体常用的构造方法有:
Semaphore(int permits)
创建具有给定的许可数和非公平的公平设置的Semapore
- 具体常用的方法有:
- acquire()从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断
- release()释放一个许可,将其返回给信号量
- 设置许可数量Semaphore semaphore = new Semaphore(3);
- 一般acquire()都会抛出异常,release在finally中执行
通过具体案例
6辆汽车,停3个车位
完整代码:
在这里插入代码片//6辆汽车,停3个车位
public class SemaphoreDemo {
public static void main(String[] args) {
//创建Semaphore,设置许可数量
Semaphore semaphore = new Semaphore(3);
//模拟6辆汽车
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));
System.out.println(Thread.currentThread().getName()+" ------离开了车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放
semaphore.release();
}
},String.valueOf(i)).start();
}
}
}
**
8. 读写锁
-
回顾悲观锁和乐观锁的概念
- 悲观锁:单独每个人完成事情的时候,执行上锁解锁。解决并发中的问题,不支持并发操作,只能一个一个操作,效率低
- 乐观锁:每执行一件事情,都会比较数据版本号,谁先提交,谁先提交版本号
-
新概念:
- 表锁:整个表操作,不会发生死锁
- 行锁:每个表中的单独一行进行加锁,会发生死锁
- 读锁:共享锁(可以有多个人读),会发生死锁
- 写锁:独占锁(只能有一个人写),会发生死锁
- 读写锁:一个资源可以被多个读线程访问,也可以被一个写线程访问,但不能同时存在读写线程,读写互斥,读读共享
-
- 读写锁ReentrantReadWriteLock
- 读锁为ReentrantReadWriteLock.ReadLock,readLock()方法
- 写锁为ReentrantReadWriteLock.WriteLock,writeLock()方法
- 创建读写锁对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();
- 写锁 加锁 rwLock.writeLock().lock();,解锁为rwLock.writeLock().unlock();
- 读锁 加锁rwLock.readLock().lock();,解锁为rwLock.readLock().unlock();
-
案例分析:
模拟多线程在map中取数据和读数据
完整代码如图
//资源类
class MyCache {
//创建map集合
private volatile Map<String,Object> map = new HashMap<>();
//创建读写锁对象
private ReadWriteLock rwLock = new ReentrantReadWriteLock();
//放数据
public void put(String key,Object value) {
//添加写锁
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+" 正在写操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
//放数据
map.put(key,value);
System.out.println(Thread.currentThread().getName()+" 写完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放写锁
rwLock.writeLock().unlock();
}
}
//取数据
public Object get(String key) {
//添加读锁
rwLock.readLock().lock();
Object result = null;
try {
System.out.println(Thread.currentThread().getName()+" 正在读取操作"+key);
//暂停一会
TimeUnit.MICROSECONDS.sleep(300);
result = map.get(key);
System.out.println(Thread.currentThread().getName()+" 取完了"+key);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放读锁
rwLock.readLock().unlock();
}
return result;
}
}
public class ReadWriteLockDemo {
public static void main(String[] args) throws InterruptedException {
MyCache myCache = new MyCache();
//创建线程放数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.put(num+"",num+"");
},String.valueOf(i)).start();
}
TimeUnit.MICROSECONDS.sleep(300);
//创建线程取数据
for (int i = 1; i <=5; i++) {
final int num = i;
new Thread(()->{
myCache.get(num+"");
},String.valueOf(i)).start();
}
}
}
- 总结锁的演变
无锁:多线程抢夺资源
synchronized和ReentrantLock,都是独占,每次只可以一个操作,不能共享
ReentrantReadWriteLock,读读可以共享,提升性能,但是不能多人写。缺点:造成锁饥饿问题(一直读,不能写),读进程不能写,写进程可以读。
写锁降级为读锁(一般等级写锁高于读锁) - 具体降级步骤
获取写锁->获取读锁->释放写锁->释放读锁
//演示读写锁降级
public class Demo1 {
public static void main(String[] args) {
//可重入读写锁对象
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//读锁
ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//写锁
//锁降级
//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");
//2 获取读锁
readLock.lock();
System.out.println("---read");
//3 释放写锁
writeLock.unlock();
//4 释放读锁
readLock.unlock();
}
}
如果是读之后再写,执行不了
因为读锁权限小于写锁
需要读完之后释放读锁,在进行写锁
//2 获取读锁
readLock.lock();
System.out.println("---read");
//1 获取写锁
writeLock.lock();
System.out.println("manongyanjiuseng");
9. 阻塞队列
- 而阻塞队列是共享队列(多线程操作),一端输入,一端输出
不能无限放队列,满了之后就会进入阻塞,取出也同理- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变得空闲起来并后续新增
9.1 种类
-
ArrayBlockingQueue
基于数组的阻塞队列
由数组结构组成的有界阻塞队列
-ArrayBlockingQueue 在生产者放入数据和消费者获取数据,都是共用同一个锁对象,无法并行 -
LinkedBlockingQueue
基于链表的阻塞队列
由链表结构组成的有界(但大小默认值为integer.MAX_VALUE)阻塞队列
之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能
3.DelayQueue
使用优先级队列实现的延迟无界阻塞队列
DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞
4.PriorityBlockingQueue
基于优先级的阻塞队列
支持优先级排序的无界阻塞队列
不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者
5.SynchronousQueue
一种无缓冲的等待队列
相对于有缓冲的 BlockingQueue 来说,少了一个中间经销商的环节(缓冲区)
不存储元素的阻塞队列,也即单个元素的队列
声明一个 SynchronousQueue 有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
• 公平模式:SynchronousQueue 会采用公平锁,并配合一个 FIFO 队列来阻塞
多余的生产者和消费者,从而体系整体的公平策略;
• 非公平模式(SynchronousQueue 默认):SynchronousQueue 采用非公平锁,同时配合一个 LIFO 队列来管理多余的生产者和消费者
而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理
6.LinkedTransferQueue
由链表结构组成的无界阻塞 TransferQueue 队列
由链表组成的无界阻塞队列
预占模式。意思就是消费者线程取元素时,如果队列不为空,则直接取走数据,若队列为空,生成一个节点(节点元素为 null)入队,消费者线程被等待在这个节点上,生产者线程入队时发现有一个元素为 null 的节点,生产者线程就不入队了,直接就将元素填充到该节点,并唤醒该节点等待的线程,被唤醒的消费者线程取走元素,从调用的方法返回
7.LinkedBlockingDeque
由链表结构组成的双向阻塞队列
阻塞有两种情况
插入元素时: 如果当前队列已满将会进入阻塞状态,一直等到队列有空的位置时再该元素插入,该操作可以通过设置超时参数,超时后返回 false 表示操作失败,也可以不设置超时参数 一直阻塞,中断后抛出 InterruptedException异常
读取元素时: 如果当前队列为空会阻塞住直到队列不为空然后返回元素,同样可以通过设置超时参数
9.2 方法
-
创建阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
-
加入元素
System.out.println(blockingQueue.add("a"));
,成功为true,失败为false -
检查元素
System.out.println(blockingQueue.element());
-
取出元素
System.out.println(blockingQueue.remove());
,先进先出
-
第二种方法:
- 加入元素
System.out.println(blockingQueue.offer("a"));
- 取出元素
System.out.println(blockingQueue.poll());
- 加入元素
-
第三种方法:
- 加入元素blockingQueue.put(“a”);
- 取出元素System.out.println(blockingQueue.take());
该方法加入元素或者取出元素,如果满了或者空了,还进行下一步加入或者取出操作,会出现阻塞的状态,而第一二种方法是直接抛出异常
- 第四种方法:
- 加入元素System.out.println(blockingQueue.offer(“a”));
- 该方法满了或者空了在进行会有阻塞,但可以加入参数,超时退出System.out.println(blockingQueue.offer(“w”,3L, TimeUnit.SECONDS));
10. 线程池
- 连接池概念
- 连接池是创建和管理一个连接的缓冲池的技术,这些连接准备好被任何需要它们的线程使用
- 线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。
这避免了在处理短时间任务时创建与销毁线程的代价
。线程池不仅能够保证内核的充分利用,还能防止过分调度
- 特点:
降低资源消耗
: 通过重复利用已创建的线程降低线程创建和销毁造成的销耗。提高响应速度
: 当任务到达时,任务可以不需要等待线程创建就能立即执行。提高线程的可管理性:
线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 具体架构:
- Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 Executor,Executors,ExecutorService,ThreadPoolExecutor 这几个类
- 说明:Executors为工具类,I为接口类,C为实现类
10.1 种类与创建
Executors.newFixedThreadPool(int)一池N线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口
Executors.newSingleThreadExecutor()一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
Executors.newCachedThreadPool()一池可扩容根据需求创建线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();
执行线程execute()
关闭线程shutdown()
void execute(Runnable command);参数为Runnable接口类,可以通过设置lambda
具体案例代码案例
//演示线程池三种常用分类
public class ThreadPoolDemo1 {
public static void main(String[] args) {
//一池五线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5); //5个窗口
//一池一线程
ExecutorService threadPool2 = Executors.newSingleThreadExecutor(); //一个窗口
//一池可扩容线程
ExecutorService threadPool3 = Executors.newCachedThreadPool();
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
//执行
threadPool3.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//关闭
threadPool3.shutdown();
}
}
}
10.2 底层原理
- 通过查看上面三种方式创建对象的类源代码
都有new 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;
}
-
具体代码中的七个参数讲解:
int corePoolSize, 常驻线程数量(核心) int maximumPoolSize,最大线程数量 long keepAliveTime,TimeUnit unit,线程存活时间 BlockingQueue<Runnable> workQueue,阻塞队列(排队的线程放入) ThreadFactory threadFactory,线程工厂,用于创建线程 RejectedExecutionHandler handler拒绝测试(线程满了)
-
具体工作流程是:
- 在执行创建对象的时候不会创建线程
- 创建线程的时候execute()才会创建
- 先到常驻线程,满了之后再到阻塞队列进行等待,阻塞队列满了之后,在往外扩容线程,扩容线程不能大于最大线程数。大于最
- 线程数和阻塞队列之和后,会执行拒绝策略。
- 阻塞队列为3,常驻线程数2,最大线程数5
-
具体的拒绝策略有:
- 抛异常
- 谁调用找谁
- 抛弃最久执行当前
- 不理不问
10.3 自定义线程池
- 实际在开发中不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,规避资源耗尽风险
// 自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
其他都同理,只是调用ThreadPoolExecutor类,自定义参数
完整代码演示
//自定义线程池创建
public class ThreadPoolDemo2 {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
2L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
//10个顾客请求
try {
for (int i = 1; i <=10; i++) {
//执行
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName()+" 办理业务");
});
}
}catch (Exception e) {
e.printStackTrace();
}finally {
//关闭
threadPool.shutdown();
}
}
}
11. Fork与Join分支
- 将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果
class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}
-
该算法相当于递归,且是二分查找思路
- ForkJoinTask:我们要使用 Fork/Join 框架,首先需要创建一个 ForkJoin 任务。该类提供了在任务中执行 fork 和 join 的机制。通常情
- 下我们不需要直接集成 ForkJoinTask 类,只需要继承它的子类,Fork/Join 框架提供了两个子类:
- RecursiveAction:用于没有返回结果的任务
- RecursiveTask:用于有返回结果的任务
- ForkJoinPool:ForkJoinTask 需要通过 ForkJoinPool 来执行
- RecursiveTask: 继承后可以实现递归(自己调自己)调用的任务
-
创建分支合并对象
-
通过该对象调用内部方法
具体案例:1加到100,相加两个数值不能大于10
完整代码如图
class MyTask extends RecursiveTask<Integer> {
//拆分差值不能超过10,计算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 = result+i;
}
} else {//进一步拆分
//获取中间值
int middle = (begin+end)/2;
//拆分左边
MyTask task01 = new MyTask(begin,middle);
//拆分右边
MyTask task02 = new MyTask(middle+1,end);
//调用方法拆分
task01.fork();
task02.fork();
//合并结果
result = task01.join()+task02.join();
}
return result;
}
}
public class ForkJoinDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//创建MyTask对象
MyTask myTask = new MyTask(0,100);
//创建分支合并池对象
ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> forkJoinTask = forkJoinPool.submit(myTask);
//获取最终合并之后结果
Integer result = forkJoinTask.get();
System.out.println(result);
//关闭池对象
forkJoinPool.shutdown();
}
}
12. 异步回调
- CompletableFuture 在 Java 里面被用于异步编程,异步通常意味着非阻塞,可以使得我们的任务单独运行在与主线程分离的其他线程中,并且通过回调可以在主线程中得到异步任务的执行状态,是否完成,和是否异常等信息
- 类中的具体引用类以及接口:
- CompletableFuture 实现了 Future, CompletionStage 接口,实现了 Future接口就可以兼容现在有线程池框架,而 CompletionStage 接口才是异步编程的接口抽象,里面定义多种异步方法,通过这两者集合,从而打造出了强大的CompletableFuture 类
- 异步调用没有返回值方法runAsync
- 异步调用有返回值方法supplyAsync
- 主线程调用 get 方法会阻塞
具体完整代码演示:
//异步调用和同步调用
public class CompletableFutureDemo {
public static void main(String[] args) throws Exception {
//y异步调用调用
CompletableFuture<Void> completableFuture1 = CompletableFuture.runAsync(()->{
System.out.println(Thread.currentThread().getName()+" : CompletableFuture1");
});
completableFuture1.get();
//mq消息队列
//异步调用
CompletableFuture<Integer> completableFuture2 = CompletableFuture.supplyAsync(()->{
System.out.println(Thread.currentThread().getName()+" : CompletableFuture2");
//模拟异常
int i = 10/0;
return 1024;
});
completableFuture2.whenComplete((t,u)->{
System.out.println("------t="+t);//返回值
System.out.println("------u="+u);//异常
}).get();
}
}
12.1 Future 与 CompletableFuture
对比这两种方法,一个为同步一个为异步
Futrue 在 Java 里面,通常用来表示一个异步任务的引用,比如我们将任务提交到线程池里面,然后我们会得到一个 Futrue,在 Future 里面有 isDone 方法来 判断任务是否处理结束,还有 get 方法可以一直阻塞直到任务结束然后获取结果,但整体来说这种方式,还是同步的,因为需要客户端不断阻塞等待或者不断轮询才能知道任务是否完成
(1)不支持手动完成
我提交了一个任务,但是执行太慢了,我通过其他路径已经获取到了任务结果,现在没法把这个任务结果通知到正在执行的线程,所以必须主动取消或者一直等待它执行完成
(2)不支持进一步的非阻塞调用
通过 Future 的 get 方法会一直阻塞到任务完成,但是想在获取任务之后执行额外的任务,因为 Future 不支持回调函数,所以无法实现这个功能
(3)不支持链式调用
对于 Future 的执行结果,我们想继续传到下一个 Future 处理使用,从而形成一个链式的 pipline 调用,这在 Future 中是没法实现的。
(4)不支持多个 Future 合并
比如我们有 10 个 Future 并行执行,我们想在所有的 Future 运行完毕之后,执行某些函数,是没法通过 Future 实现的。
(5)不支持异常处理
Future 的 API 没有任何的异常处理的 api,所以在异步运行时,如果出了问题是不好定位的