JUC并发编程(一)

63 阅读11分钟

并发

  • 是在同一实体上的多个事件
  • 是在同一台处理器上“同时”处理多个任务(一对多)
  • 同一时刻,其实是只有一个事件在发生

并行

  • 是在不同实体上的多个事件
  • 是在多台处理器上同时处理多个任务(多对多)
  • 同一时刻,大家都在同时做自己的事情

用户线程

  • 是系统的工作线程,他会完成这个程序需要完成的业务操作,默认情况下都是用户线程

守护线程

  • 是一种特殊的线程,为其他的线程服务,例如:==垃圾回收线程==
  • 守护线程作为一个服务线程,在用户线程全部结束的时候,它也就自动结束了

CompletableFuture

Future接口

  • 异步任务接口,可以为主线程开启一个分支任务,专门为主线程处理耗时和费力的复杂业务

FutureTask(Future接口实现类)

  • 在这里插入图片描述

  • 案例(老师交代学生去买水,学生买水回来通知老师)

    /**
     * 业务背景,班长为老师去买水,作为新启动的异步多线程任务,且买到水有结果返回
     * 1、main方法就是主线程,代表老师
     * 2、futureTask代表学生,学生代替老师买水
     * **/
    public class CompleteFutureDemo {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            FutureTask<String> futureTask = new FutureTask<String>(new MyThread2());
    
            Thread thread = new Thread(futureTask,"t1");
            thread.start();
    
    
            System.out.println(futureTask.get());
    
        }
    
    
    }
    
    class MyThread2 implements Callable<String> {
    
        @Override
        public String call() throws Exception {
            System.out.println("---------- come in call()");
            return "hello teacher 我买水回来了 ";
        }
    }
    
    
  • 结合Executors线程池(避免频繁的生成Thread对象引起性能浪费)

    public class FutureThreadPoolDemo {
    
        public static void main(String[] args) throws ExecutionException, InterruptedException {
    
            ExecutorService threadPool = Executors.newFixedThreadPool(3);
            long startTime = System.currentTimeMillis();
    
            //3个任务,目前开启多个异步线程任务来处理,耗时多久?
            FutureTask<String> futureTask1 = new FutureTask<String>(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "task1 over";
            });
    
            threadPool.submit(futureTask1);
    
    
            FutureTask<String> futureTask2 = new FutureTask<String>(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "task2 over";
            });
    
            threadPool.submit(futureTask2);
    
    
            System.out.println(futureTask1.get());
            System.out.println(futureTask2.get());
    
    
            //主线程运行任务3
            //任务3
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
    
            System.out.println("花费时间" + (endTime - startTime) / 1000 + "毫秒");
            threadPool.shutdown();
    
    
        }
    
        public static void m1() {
            //3个任务,只有一个main线程来处理,耗时多久?
            long startTime = System.currentTimeMillis();
            //暂停毫秒
    
    
            //任务1
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //任务2
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //任务3
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
    
            long endTime = System.currentTimeMillis();
    
            System.out.println("花费时间" + (endTime - startTime) / 1000 + "毫秒");
        }
    }
    
  • 缺点:FutureTask.get()会导致线程的阻塞 ,使用isDone()轮询,会消耗CPU资源

CompletableFuture

  • 在这里插入图片描述
public class CompletableFutureBuildDemo {

    public static void main(String[] args) throws ExecutionException, InterruptedException {


        ExecutorService threadPool = Executors.newFixedThreadPool(3);


        /**
         * runAsync(无返回值)
         * **/
        CompletableFuture<Void> completableFuture = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName());
            //业务代码
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },threadPool);

        System.out.println(completableFuture.get());

        /**
         * supplyAsync(有返回值)
         * **/
        CompletableFuture<String> completableFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println(Thread.currentThread().getName());
            //业务代码
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "hello supplyAsync";
        },threadPool);
        System.out.println(completableFuture.get());

        threadPool.shutdown();
    }
}
  • 注意:在使用的时候一定要自定义线程池threadPool, 否则使用默认ForkJoinPool它是守护线程,在主线程结束的话,它也随之结束。
  • 如果你执行第一个任务的时候,传入了了一个自定义线程池,调用thenRun方法执行第二个任务的时候,则第二个任务和第一个任务时公用同一个线程池,调用thenRunAsync执行第二个任务的时候,则第一个任务调用的时自己传入的线程池,第二个任务使用的默认ForkJoinPool线程池,只要是方法后面跟着Async的其他方法都是同理
  • completableFuture.get()会引起阻塞,而且还会抛出异常
  • completableFuture.join()也会引起阻塞,但是不会抛出异常
  • completableFuture.get(long timeout, TimeUnit unit),超过了指定时间,直接抛出异常
  • completableFuture.getNow(T t)如果没有计算完成,返回一个替代结果,如果计算完成后,返回特定的结果
  • 对计算结果进行处理
    • thenApply()对计算结果的一个处理,但是在遇到异常时会直接抛出,导致程序在当前步骤停止
    • handle() 也是对计算结果的一个处理,和thenApply的区别就是,在有异常时,下面的步骤还会进行,不会立即停止,最终抛出异常。
  • 对计算结果进行消费
    • thenAccept()接收任务的处理结果,并消费处理,无返回结果

函数式接口

在这里插入图片描述

悲观锁

  • 认为自己在使用数据的时候一定有别的线程来修改数据,因为在获取数据的时候一定会先获取锁,确保数据不会被其他线程所修改。(适合写操作的场景)
    • synchronized关键字
    • Lock的实现类

乐观锁

  • 认为自己在使用数据的时候不会有别的线程来修改数据或资源,所以不会加锁,在java编程中使用的是无锁编程来实现的,只是在更新数据的时候去判断,有没有别的线程去修改当前数据。如果数据没更新,当前线程将自己的修改操作进行写入,如果更新了的话则根据不同的方式执行不同的操作,比如放弃修改、或者重试抢锁等等(适合读操作)
  • 判断规则
    • 版本号机制Version
    • 最常采用的是CAS算法,Java原子类的递增操作就是通过CAS自旋来实现的

原理解释

  • 一个对象中有多个synchronized的方法,某一个时刻内,只要一个线程去访问synchronized方法,其他的线程只能等待,锁的是当前==对象==this,被锁定后,其他的线程不能进入到该对象中的其它synchronized方法中。
  • 普通方法和同步锁无关,如果是多个对象的话,拥有的就是各是各的锁,不会发生上述的等待情况
  • 对于普通同步方法,锁的是当前的==实例对象==,通常指的this,对于静态(加入)static关键字的同步方法,锁的是当前类的==Class==
  • 当一个线程访问同步代码块的时候,必须要先获取锁,正常退出或者抛出错误,是要释放锁
  • 普通的同步方法,用的都是同一把锁,实例对象本身,就是new出来的具体实例对象,也就是说如果一个实例对象的普通同步方法获取锁后,该实例对象的其他同步方法,必须等待获取锁的方法释放锁后,才能再去获取锁
  • 所有的==静态同步==方法,用的也都是同一把锁,但是用的是==类==本身
  • 具体==实例对象==和==类==,这两把锁是两个不同对象,所以静态同步块和普通方法之间不会产生竞态条件,但是一个一个静态同步方法获得锁以后,其他的静态同步方法必须等待。
  • 以上总结
    • 作用于实例方法,给当前实例对象加锁,进入同步代码块前要获得当前实例对象的锁
    • 对于代码块,给括号里的配置对象加锁
    • 作用于静态方法,当前类加锁,进入同步代码块前要获得当前类对象的

为什么每个对象都可以成为一个锁

  • 每个对象都天生带着一个对象的监视器
  • 每个被锁住的对象都会和Monitor关联起来

公平锁

  • 是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人后买。这是公平的
  • ReentrantLock lock = new ReentrantLock(true);

非公平锁(效率要高于公平锁,因为避免了频繁的线程上下文切换)

  • 是指多个线程按照不申请锁的顺序来获取锁,有可能后申请的线程比先申请的线程优先获取锁,在高并发的情况下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直获取不到锁)

  • ReentrantLock lock = new ReentrantLock(false);或者ReentrantLock lock = new ReentrantLock();默认情况下是非公平的

可重入锁(递归锁)

  • 是指在同一线程在外层方法获取到锁的时侯,在进入该线程的内层方法会自动获取锁(前提,锁对象的是同一个对象),不会因为之前已经获取过还没释放而阻塞---------优点之一就是可一定程度避免死锁。
  • 隐式锁(synchronized默认是可重入锁)
    • 原理:当执行monitorenter时,如果目标锁对象的计数器为0时,那么说明他没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,那么计数器+1,当目标对象计数器不为0时,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器+1,否则需要等待,直至持有线程释放该锁。当执行monitorexit时,Java虚拟机需要将锁对象的计数器-1,计数器为0时,代表该锁已被释放
  • 显示锁(Lock)也有ReentrantLock这样的可重入锁

死锁及排查

  • 死锁时两个或者两个线程以上在执行的过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那么他们将永远无法推进下去。
  • 原因
    • 系统资源不足
    • 进程运行推进的顺序不合适
    • 资源分配不当

加锁过程

在这里插入图片描述

解锁过程

在这里插入图片描述

LockSupport与线程中断

线程中断

  • 一般一个线程不应该由其他线程强制中断,而是由线程自行停止
  • 在Java中没有办法立即停止一条线程,然而停止线程却显得尤为重要,如取消一个耗时操作。因此Java就提供了一种利用停止线程的协商机制——中断,也叫==中断标识协商机制==
  • 若要中断一个线程,需要手动调用该线程的interrupt()方法,该方法也仅仅是将中断标识设置为true。接着自己写代码不断得检测当前线程得标识位,如果为true,表示别的线程请求这条线程中断。
  • 三大中断方法说明
    • void interrupt(),仅仅是设置线程得中断状态为true,发起协商而不会==立刻停止线程==,中断不活动的线程,不会产生任何影响。
    • static boolean interrupted() ,判断线程是否被中断并清除当前中断状态
      • 1、返回当前线程的中断状态,测试当前线程是否被打断
      • 2、将当前线程的中断状态清零并重新设置为false,清除线程的中断状态
    • boolean isinterrupted() ,判断当前线程是否被中断(通过检查中断标识位)
  • 调用sleepjoin或者wait方法时,中断状态会被清除,会抛出InterruptedException异常,所以必须得在catch块中再次调用Thread.interrupt()

LockSupport

  • 用于创建锁和其它同步类的基本线程阻塞原语
  • waitnotify
    • waitnotify必须得在同步块或者方法中使用,且成对出现
    • 必须得先waitnotify
  • signalawait
    • Condition中的线程等待和唤醒方法,需要先获得锁
    • 一定要先awaitsignal
  • park阻塞和unpark唤醒
    • 调用park方法时,如果有凭证,则会消耗这个凭证,正常退出。如果没有凭证,就必须阻塞等待凭证可用
    • 而unpark正好相反,它会增加一个凭证,但凭证最多只能有一个,累加无效

JMM

在这里插入图片描述

  • JMM内存模型,本身是一种抽象的概念并不真实存在,它仅仅描述的是一组约定或规范,通过这组规范定义了程序中的各个变量的读写访问方式并决定一个线程对共享变量的写入何时以及如何对另一个线程可见,关键技术点都是围绕多线程的的==原子性、可见性和有序性==展开的
    • 可见性:是指当一个线程如果修改了某一共享变量值,其它线程是否能够立即知道该变更,JMM规定了所有的变量都存储在主内存中。
    • 原子性:指一个操作是不可被打断的,即多线程环境下,操作不能被其他线程干扰
    • 有序性:为了提升性能,编译器和处理器通常会对指令进行重新排序。Java规范规定JVM线程内部维持顺序话语义,即只要程序的最终结果与它顺序话执行的结果一致,那么指令执行的顺序可以和代码的执行顺序不一致,此过程叫做指令重排。
  • 所有的线程在操作主内存中的变量时,都必须首先将变量拷贝到自己线程的工作内存中去操作,然后再由JMM更新到主内存中
  • happens-before
    • 在JMM中,如果一个操作执行的结果需要对另一个操作可见性或者代码重排序,那么这两个操作之间肯定有happens-before(先行发生原则),逻辑上的先后关系
    • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个执行结果操作可见,而且第一个操作的执行顺序排在第二个操作之前
    • 两个操作之间存在happens-before关系,并不意味着一定要按照happens-before原则制订的顺序来执行。如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序==并不非法==