JUC并发编程

本文详细介绍了Java并发编程的基础知识,包括线程、进程、用户线程与守护线程、Future接口和CompletableFuture的使用及其改进,以及乐观锁、悲观锁、公平锁与非公平锁的区别,还有可重入锁和死锁的原理与排查。
摘要由CSDN通过智能技术生成

JUC 并发编程


一、基础知识

1start线程开启 底层源码分析

在这里插入图片描述

2. “1”把锁 (Synchronized)“2”个并 “3”个程

“2”个并

  两个并是指“并发”和“并行”。并发,是在同一实体上的多个事件;在同一台处理器上“同时”处理多个任务;同一时刻,其实是只有一个事件在发生(抢票、秒杀等同一时刻在服务器上面同时处理多个请求抢购的功能)。并行,是在不同实体上的多个事件;在多台处理器上同时处理多个任务;同一时刻,大家各做各的事情,但是都在做事情(泡方便面:一边烧热水,一边在撕调味包,各自独立,并行处理)。

“3”个程

进程,在系统中运行的一个应用程序就是一个进程,每个进程都有它自己的内存空间和系统资源。
线程,轻量级进程,在同一个进程内会有1个或多个线程,是大多数操作系统进行时序调度的基本单元。如下idea应用程序和其运行的资源就是进程和线程
在这里插入图片描述
管程,Monitor(监视器),也就是我们平时所说的锁。执行线程就要求先成功持有管程(锁),然后才能执行方法,最后当方法完成(无论是正常完成还是非正常完成)时释放管程(锁)。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同一方法所持有的管程将在异常抛到同步方法边界之外时自动释放。
在这里插入图片描述

3. 用户线程和守护线程

用户线程(User Thread)

  一般情况下不做特别说明配置,默认都是用户线程,用户线程是系统的工作线程,它会完成这个程序需要完成的业务操作

守护线程(Daemon Thread)

 是一种特殊的线程为其它线程服务的,在后台默默地完成一些系统的服务,比如垃圾回收线程就是最典型的例子。守护线程作为一个服务线程,没有服务对象就没有必要继续运行了,如果用户线程全部结束了,意味着程序需要完成的业务操作已经结束了,系统可以退出。所以假如当系统指剩下守护线程的时候,Java虚拟机会自动退出

  两个用户线程其中一个是否结束对另外一个没有影响。如果用户线程全部结束意味着程序需要完成的业务操作已经结束了,守护线程随着JVM一同结束。setDaemont(true)方法设置线程为守护线程必须在start()之前设置,否则报异常

二、CompletableFuture

1.Future接口理论知识复习

  Future接口(FutureTask实现类)定义了操作异步任务执行一些方法(异步操作则是指调用方发出请求后不等待结果,而是继续执行后续操作,当请求完成后,通过回调函数、事件通知、Future/Promise等方式来处理结果。),如获取异步任务的执行结果、取消任务的执行、判断任务是否初取消、判断任务执行是否完毕等。比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,忙其它事情或者先执行完,过了一会才去获取子任务的执行结果或变更的任务状态。总的来说,Future接口可以为主线程开一个分支,专门为主线程处理耗时和费力的复杂业务

2.Future接口常用实现类FutureTask(任务类)异步任务

2.1 Future接口相关架构

在这里插入图片描述

其中FutureTask实现了RunnableFuture接口,RunnableFuture接口又实现了Runable(多线程)和Future接口异步,那就使得RunnableFuture接口具备了多线程和异步的作用。FutureTask类实现了RunnableFuture接口也具备了他的功能,然后FutureTask类还通过适配器设计模式,构造方法注入了具有返回值的功能(如下),所以FutureTask类就具备了“多线程、有返回值、异步”三个特点,达到了使用Future的作用
在这里插入图片描述
具体流程如下:
在这里插入图片描述

2.2 Future接口有缺点分析

  • 优点

future+线程池异步多线程任务配合,能显著提高程序的执行效率。
在这里插入图片描述

  • 缺点
  1. 使用get()获取结果容易导致阻塞(当Future线程没有完成时,去通过get()获取结果,程序就会等待Future线程返回结果后再执行后面的任务,从而导致阻塞),因此一般建议获取结果放在程序后面(也可以设定get()等待时间,过时不候,然后报错避免堵塞;还可以通过设定状态,当执行完后给定一个状态,通过状态确定(调用isDone()方法来获取状态)执行完后就来调用get()获取结果),但是调用isDone()就会产生另外一个缺点。
  2. isDone()轮询耗费CPU,一直轮询判断很耗费CPU
  3. 无法实现“多个任务前后依赖可以组合处理”在这里插入图片描述
    由此引入CompletableFuture来优化这些问题

3.CompletableFuture对Future的改进

3.1 CompletableFuture为什么出现

在这里插入图片描述

3.2 CompletableFuture和CompletionStage源码分别介绍

  • CompletableFuture源码

CompletableFuture实现两个接口
在这里插入图片描述
获取结果和触发计算的源码方法
在这里插入图片描述
对计算结果进行处理的源码方法
在这里插入图片描述
在这里插入图片描述
对计算结果进行消费的源码方法
在这里插入图片描述
在这里插入图片描述
对计算速度进行选用的源码方法
在这里插入图片描述
在这里插入图片描述
对计算结果进行合并的源码方法
在这里插入图片描述
在这里插入图片描述
join和get的区别:
  都是获取异步执行返回的结果,但是get需要抛出异常,join不需要抛出异常
在这里插入图片描述

  • CompletionStage(爬楼梯,一步一步走)

在这里插入图片描述

3.3 核心的四个静态方法,来创建一个异步任务(一般不推荐new一个对象)

在这里插入图片描述
举例如下:
无返回值:
在这里插入图片描述
有返回值:
在这里插入图片描述
在这里插入图片描述
优点:
在这里插入图片描述

4.案例精讲-从电商网站的比价需求出发

4.1 函数式接口

在这里插入图片描述

4.2 案例讲解

在这里插入图片描述

public class CompletableFutureDemo {
    // 声明店铺集合
    static List<NetMall> list = Arrays.asList(
            new NetMall("京东"),
            new NetMall("淘宝"),
            new NetMall("得物")
    );
    /*
     * 一家一家搜查
     * 传递店铺名集合和商品名两个参数
     */
    public static List<String> getPrice(List<NetMall> list, final String productName){
        return list
                .stream()
                .map(netMall ->
                        String.format(productName + " in %s price is %.2f",
                        netMall.getNetMallName(),
                        netMall.calcPrice(productName)))
                .collect(Collectors.toList());
    }


    /*
    * 异步执行
    * List<NetMall> -------> List<CompletableFuture<String>> -------> List<String>
     */
    public static List<String> getPriceByCompletableFuture(List<NetMall> list,String productName){
        return list.stream().map(netMall ->
                CompletableFuture.supplyAsync(()->String.format(productName + " in %s price is %.2f",
                                netMall.getNetMallName(),
                                netMall.calcPrice(productName))))
                .collect(Collectors.toList())
                .stream()
                .map(s->s.join())
                .collect(Collectors.toList());
    }
    public static void main(String args[]){
        long startTime = System.currentTimeMillis();
        List<String> list1 = getPriceByCompletableFuture(list,"mysql");
        for(String element : list1){
            System.out.println(element);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime-startTime)+"毫秒");
        System.out.println("---------------------------------------------");
        long startTime2 = System.currentTimeMillis();
        List<String> list2 = getPrice(list,"mysql");
        for(String element : list2){
            System.out.println(element);
        }
        long endTime2 = System.currentTimeMillis();
        System.out.println("----costTime:"+(endTime2-startTime2)+"毫秒");

    }
}

/**
 * 获取商品价格的工具类
 */
class NetMall{
    // 店铺名
    private String netMallName;
    
    public NetMall(String netMallName){
        this.netMallName = netMallName;
    }
    // 获取店铺中商品的价格
    public double calcPrice(String productName){
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return ThreadLocalRandom.current().nextDouble() * 2 + productName.charAt(0);
    }

    public String getNetMallName() {
        return netMallName;
    }

    public void setNetMallName(String netMallName) {
        this.netMallName = netMallName;
    }
}

在这里插入图片描述

三、JAVA锁

1.乐观锁和悲观锁

  • 悲观锁

  悲观情绪,认为自己在使用数据的时候一定有别的线程来修改共享数据,因此在获取数据的时候会先加锁,这样其他线程想要拿到这个资源就会阻塞直到锁被上一个持有者释放,确保数据不会被别的线程修改也就是说,共享资源每次只给一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。synchronized关键字和ReentrantLock的实现类都是悲观锁
  适合写操作多的场景,先加锁可以保证写操作时数据正确。显式的锁定之后再操作同步资源
缺点就是在高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能存在死锁问题,影响代码的正常运行。
在这里插入图片描述

  • 乐观锁

  认为自己在使用数据时不会有别的线程修改数据或资源,所以不会添加锁。在Java中是通过使用无锁编程来实现,只是在更新数据的时候去判断,之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果这个数据已经被其它线程更新,则根据不同的实现方式执行不同的操作,比如放弃修改、重试抢锁等等
  适合读操作多的场景,不加锁的特点能够使其读操作的性能大幅提升。乐观锁则直接去操作同步资源,是“种无锁算法,得之我幸不得我命.
  高并发场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更甚一筹。但是,如果冲突频繁发生(写操作占比非常多的时候),会频繁失败和重试,这样会非常影响性能,导致cpu飙升。
在这里插入图片描述

2.关于“锁”的案例演示——加深“锁”的概念

在这里插入图片描述
在这里插入图片描述

  •   资源实例对象(资源类new的对象)里面有多个synchronized方法,如果某一时刻内有一个线程访问某个synchronized方法,那么这个资源实例对象全部被锁(其他线程不能访问),而不只是锁被访问的这个synchronized方法,所以当不同线程访问这个资源实例对象不同synchronized方法时,不存在“各回各家,各找各妈”的事情,只有后一个线程等待前一个线程释放锁后才能访问。
    在这里插入图片描述

  •   加个普通方法后和同步锁无关,访问普通方法不受锁的限制。换成两个不同的对象后,不是同一把锁了,不会存在锁的占用。

  • 方法都换成静态同步方法后,情况又变化,三种synchronized锁的内容有一些差别:
      对于普通同步方法,锁的是当前实例对象,通常指this,具体的一部部手机,所有的普通同步方法用的都是同一把锁—>实例对象本身;对于静态同步方法,锁的是当前类的cLass对象,如IPhone.class唯一的一个模板;对于同步方法块,锁的是synchronized 括号内的对象

  • 当一个线程试图访问同步代码时它首先必须得到锁,正常退或抛出异常时必须释放锁
      所有的普通同步方法用的都是同一把锁——实例对象本身,就是new出来的具体实例对象本身,本类this,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他普通同步方法必须等待获取锁的方法释放后才能获取锁
      所有的静态同步方法用的也是同一把锁——类对象本身,就是我们说过的唯一模板class,具体实例对象this和唯一模板class,这两把锁是两个不同的对象,所以静态同步方法与普通同步方法之间是不会有竞态条件的,但是一旦一个静态同步方法获取锁后,其他的静态同步方法都必须等待该方法释放锁后才能获取锁。

3.synchronized字节码分析

synchronized锁主要作用于实例方法代码块静态方法三个地方,所以源码分析也从这三个地方进行

3.1 synchronized同步代码块 (实现使用的是monitorenter和monitorexit指令)

在这里插入图片描述
反编译class文件:
在这里插入图片描述
一般有两个释放,第一个释放是正常执行正常释放;第二个是为了防止出现错误后没有释放锁,在错误后释放锁并报错(自己在里面加一个抛出异常就只有一个释放)


3.2 synchronized普通同步方法

在这里插入图片描述
反编译class文件:
在这里插入图片描述
  调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置。如果设置了,执行线程会将先持有monitor锁,然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor


3.3synchronized静态同步方法

在这里插入图片描述
反编译class文件:
在这里插入图片描述
ACC_STATIC,ACC_SYNCHRONIZED访问标志区分该方法是否为静态同步方法


面试题:为什么任何一个对象都可以成为一个锁?

  在HotSpot虚拟机中,monitor(管程,锁)采用objectMonitor实现,ObjectMonitor.java→ObjectMonitor.cpp-objectMonitor.hpp,通过底层源码可以发现每个对象天生都带着一个对象监视器(来记录相关的信息,当被锁时,就会记录),每一个被锁住的对象都会和Monitor关联起来,所以任何一个对象都可以成为一个锁。
在这里插入图片描述

4.公平锁和非公平锁

默认情况下,两个线程在Java程序中是并发执行且彼此独立的,即异步执行
在这里插入图片描述

面试题:为什么会有公平锁/非公平锁的设计?为什么默认非公平锁?

恢复挂起的线程到真正锁的获取还是有时间差的,从开发人员来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用CPU的时间片,尽量减少CPU空闲状态时间
  使用多线程很重要的考量点是线程切换的开销,采用非公平锁时,当1个线程请求锁获取同步状态,然后释放同步状态,所以刚释放锁的线程在此刻再次获取同步状态的概率就变得非常大,所以就减少了线程的开销。

什么时候用公平?什么时候用非公平?

  如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省了很多线程切换时间,吞吐量自然就上去了;否则那就用公平锁,大家公平使用。

5.可重入锁(递归锁)

  是指同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),你会因为之前已经获取过还没释放而阻塞如果是1个有synchronized修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
  简单来说,可重入锁就是一个线程中的多个流程可以获取同一把锁,持有这把同步锁可以再次进入。自己可以获取自己的内部锁(在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的)
在这里插入图片描述
在这里插入图片描述

  实现原理:ObjectMonitor.java→ObjectMonitor.cpp-objectMonitor.hpp,还是这个底层源码,在对象监视器里面,当执行monitorenteri时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程并且将其计数器加1在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放

5.1 可重入锁种类

  • 隐式锁

隐式锁(即synchronized关键字使用的锁)默认是可重入锁

  • 显式锁

显式锁(即Lock)也有ReentrantLock这样的可重入锁
在这里插入图片描述
注意必须要.lock()和.unlock()配对使用

6.死锁及排查

  死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
在这里插入图片描述
在这里插入图片描述

排查死锁:

纯命令:
在这里插入图片描述
图形化:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

7.锁相关知识小总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 20
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小修士

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值